Merge branch 'dev' into feat/add_user_theme_view_controls

This commit is contained in:
lesion 2023-02-03 15:18:16 +01:00
commit 3eacc7ea33
No known key found for this signature in database
GPG key ID: 352918250B012177
117 changed files with 4890 additions and 2443 deletions

View file

@ -1,27 +1,45 @@
All notable changes to this project will be documented in this file.
### UNRELEASED
### 1.6.2 - 12 jan '23
- add swipe gesture to move to next/prev event
- fix refresh collections, fix #219
- add russian translation (thanks @drunkod)
- refactor search / filter / selection fix #225, 227, #224
- models initialization refactored, better dev experience, fix backend HMR
### 1.6.1 - 15 dec '22
- allow edit tags in admin panel, fix #170
- fix header / fallback image upload, fix #222
- fix WPGancio MU
- fix recurrent events label
- update translations (de, es, eu, gl)
### 1.6.0 - 11 dec '22
- new plugin system - fix #177
- new "publish on telegram" plugin: (thanks @fadelkon)
- i18n refactoring
- people can now choose the language displayed - fix #171
- fix place "[Object]" issue - #194
- live search
- admin could choose a custom fallback image - fix #195
- it is now possible NOT to enter the end time of an event - fix #188
- Wordpress plugin now supports MU installation
- add nominatim / openstreetmap feature (thanks @sedum)
- live search
- improve event import
- new chinese translation
- new portuguese translation
- add Apple touch icon - fix #200
- improve navbar layout
- improve event layout
- add nominatim / openstreetmap search feature (thanks @sedum)
- new hide calendar option
- new hide thumbs from homepage option
- linkable admin tab
- friendly instances label is now customizable (thanks @sedum)
- i18n refactoring
- Wordpress plugin now supports MU installation
- new chinese translation
- new portuguese translation
- improved navbar layout
- improved event layout
- complete oauth2 refactoring
- fix ics unique uuid
- fix place "[Object]" issue - #194
- fix random restart while downloading random media
- fix mobile dialog layout
- urlencode place and tag urls
### 1.5.6 - 22 set '22

View file

@ -8,7 +8,7 @@ export function attributesFromEvents(_events) {
const key = dayjs.unix(e.start_datetime).tz().format('MMDD') // Math.floor(e.start_datetime/(3600*24)) // dayjs.unix(e.start_datetime).tz().format('YYYYMMDD')
const c = (e.end_datetime || e.start_datetime) < now ? 'vc-past' : ''
if (e.multidate) {
if (e.multidate === true) {
attributes.push({
dates: { start: new Date(e.start_datetime * 1000), end: new Date(e.end_datetime * 1000) },
highlight: {

View file

@ -1,22 +1,20 @@
<template>
<nav>
<NavHeader/>
<NavHeader />
<!-- title -->
<div class='text-center'>
<nuxt-link id='title' v-text='settings.title' to='/' />
<div class='text-body-1 font-weight-light' v-text='settings.description' />
<div class="text-center">
<nuxt-link id="title" v-text="settings.title" to="/" />
<div
class="text-body-1 font-weight-light"
v-text="settings.description"
/>
</div>
<NavSearch />
<NavBar />
</nav>
</template>
<script>
import { mapState } from 'vuex'
@ -27,18 +25,24 @@ import NavSearch from './NavSearch.vue'
export default {
name: 'Appbar',
components: { NavHeader, NavBar, NavSearch },
computed: mapState(['settings'])
computed: mapState(['settings']),
}
</script>
<style>
nav {
background-image: linear-gradient(rgba(59, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), url(/headerimage.png);
background-image: linear-gradient(rgba(0, 0, 0, 0.8), rgba(20, 20, 20, 0.7)),
url(/headerimage.png);
background-position: center center;
background-size: cover;
}
.theme--light nav {
background-image: linear-gradient(to bottom, rgba(255,230,230,.95), rgba(250,250,250,.95)), url(/headerimage.png);
background-image: linear-gradient(
to bottom,
rgba(230, 230, 230, 0.95),
rgba(250, 250, 250, 0.95)
),
url(/headerimage.png);
}
#title {
@ -46,5 +50,4 @@ nav {
font-weight: 600;
text-decoration: none;
}
</style>

View file

@ -15,13 +15,14 @@
aria-label='Calendar'
is-expanded
is-inline)
template(v-slot="{ inputValue, inputEvents }")
//- template(v-slot="{ inputValue, inputEvents }")
v-btn#calendarButton(v-on='inputEvents' text tile :color='selectedDate ? "primary" : "" ') {{inputValue || $t('common.calendar')}}
v-icon(v-if='selectedDate' v-text='mdiClose' right small icon @click.prevent.stop='selectedDate = null')
v-icon(v-else v-text='mdiChevronDown' right small icon)
template(v-slot:placeholder)
v-btn#calendarButton(text tile) {{$t('common.calendar')}}
v-icon(v-text='mdiChevronDown' right small icon)
.calh.d-flex.justify-center.align-center(slot='placeholder')
v-progress-circular(indeterminate)
//- v-btn#calendarButton(text tile) {{$t('common.calendar')}}
//- v-icon(v-text='mdiChevronDown' right small icon)
</template>
@ -65,6 +66,16 @@ export default {
</script>
<style>
.vc-container.vc-is-dark {
--gray-900: #111;
--gray-700: #333;
}
.vc-container {
--gray-400: #999 !important;
--rounded-lg: 4px !important;
}
.vc-opacity-0 {
opacity: 0.3 !important;
}

View file

@ -9,7 +9,7 @@ v-dialog(v-model='show'
@keydown.esc='cancel')
v-card
v-card-title {{ title }}
v-card-text(v-show='!!message') {{ message }}
v-card-text(v-show='!!message' v-html='message')
v-card-actions
v-spacer
v-btn(outlined color='error' @click='cancel') {{$t('common.cancel')}}

View file

@ -3,12 +3,11 @@ v-col(cols=12)
.text-center
v-btn-toggle.v-col-6.flex-column.flex-sm-row(v-model='type' color='primary' @change='type => change("type", type)')
v-btn(value='normal' label="normal") {{ $t('event.normal') }}
v-btn(value='multidate' label='multidate') {{ $t('event.multidate') }}
v-btn(v-if='settings.allow_multidate_event' value='multidate' label='multidate') {{ $t('event.multidate') }}
v-btn(v-if='settings.allow_recurrent_event' value='recurrent' label="recurrent") {{ $t('event.recurrent') }}
p {{ $t(`event.${type}_description`) }}
v-btn-toggle.v-col-6.flex-column.flex-sm-row(v-if='type === "recurrent"' color='primary' :value='value.recurrent.frequency' @change='fq => change("frequency", fq)')
v-btn(v-for='f in frequencies' :key='f.value' :value='f.value') {{ f.text }}
@ -25,8 +24,9 @@ v-col(cols=12)
is-inline
is-expanded
:min-date='type !== "recurrent" && new Date()')
template(#placeholder)
span.calc Loading
//- template(#placeholder)
.d-flex.calh.justify-center(slot='placeholder')
v-progress-circular(indeterminate)
div.text-center.mb-2(v-if='type === "recurrent"')
span(v-if='value.recurrent.frequency !== "1m" && value.recurrent.frequency !== "2m"') {{ whenPatterns }}
@ -60,7 +60,7 @@ v-col(cols=12)
:allowedMinutes='allowedMinutes'
format='24hr'
@click:minute='menuFromHour = false'
@change='hr => change("fromHour", hr)')
@input='hr => change("fromHour", hr)')
v-col.col-12.col-sm-6
@ -88,14 +88,14 @@ v-col(cols=12)
:allowedMinutes='allowedMinutes'
format='24hr'
@click:minute='menuDueHour = false'
@change='hr => change("dueHour", hr)')
@input='hr => change("dueHour", hr)')
List(v-if='type === "normal" && todayEvents.length' :events='todayEvents' :title='$t("event.same_day")')
</template>
<script>
import dayjs from 'dayjs'
import { mapState } from 'vuex'
import { mapState, mapActions } from 'vuex'
import List from '@/components/List'
import { attributesFromEvents } from '../assets/helper'
import { mdiClockTimeFourOutline, mdiClockTimeEightOutline, mdiClose } from '@mdi/js'
@ -114,7 +114,6 @@ export default {
menuFromHour: false,
menuDueHour: false,
type: this.value.type || 'normal',
events: [],
frequencies: [
{ value: '1w', text: this.$t('event.each_week') },
{ value: '2w', text: this.$t('event.each_2w') },
@ -123,7 +122,7 @@ export default {
}
},
computed: {
...mapState(['settings']),
...mapState(['settings', 'events']),
fromDate () {
if (this.value.from) {
if (this.value.multidate) {
@ -139,7 +138,7 @@ export default {
return this.events.filter(e => e.start_datetime >= start && e.start_datetime <= end)
},
attributes() {
return attributesFromEvents(this.events)
return attributesFromEvents(this.events.filter(e => e.id !== this.event.id))
},
whenPatterns() {
if (!this.value.from) { return }
@ -193,13 +192,12 @@ export default {
} else {
this.type = 'normal'
}
this.events = await this.$api.getEvents({
start: dayjs().unix(),
show_recurrent: true
})
this.events = this.events.filter(e => e.id !== this.event.id)
if (!this.events) {
this.getEvents()
}
},
methods: {
...mapActions(['getEvents']),
updateRecurrent(value) {
this.$emit('input', { ...this.value, recurrent: value || null })
},
@ -235,6 +233,15 @@ export default {
} else if (what === 'dueHour') {
if (value) {
this.value.due = this.value.due ? this.value.due : this.value.from
const [hour, minute] = value.split(':')
const [fromHour, fromMinute] = this.value.fromHour.split(':')
if (!this.value.multidate) {
if (hour < fromHour) {
this.value.due = dayjs(this.value.from).add(1, 'day').toDate()
} else {
this.value.due = dayjs(this.value.from).toDate()
}
}
} else {
this.value.due = null
}

View file

@ -50,8 +50,8 @@ export default {
data ({ $store }) {
return {
mdiWalk, mdiBike, mdiCar, mdiMapMarker,
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '<a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors',
url: $store.state.settings.tilelayer_provider || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: $store.state.settings.tilelayer_provider_attribution || "<a target=\"_blank\" href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors",
zoom: 14,
center: [this.event.place.latitude, this.event.place.longitude],
marker: {

View file

@ -1,19 +1,51 @@
<template lang="pug">
#navsearch.mt-2.mt-sm-4(v-if='showCollectionsBar || showSearchBar')
v-text-field.mx-2(v-if='showSearchBar' outlined dense hide-details :placeholder='$t("common.search")' :append-icon='mdiMagnify' @input='search' clearable :clear-icon='mdiClose')
template(v-slot:prepend-inner)
Calendar(v-if='!settings.hide_calendar')
v-btn.ml-2.mt-2.gap-2(v-if='showCollectionsBar' small outlined v-for='collection in collections' color='primary' :key='collection.id' :to='`/collection/${encodeURIComponent(collection.name)}`') {{collection.name}}
#navsearch.mt-2.mt-sm-4(v-if='showCollectionsBar || showSearchBar || showCalendar')
div.mx-2
client-only(v-if='showSearchBar')
v-menu(offset-y :close-on-content-click='false' tile)
template(v-slot:activator="{on ,attrs}")
v-text-field(hide-details outlined
:placeholder='$t("common.search")'
@input="v => setFilter(['query', v])" clearable :clear-icon='mdiClose')
template(v-slot:append v-if='settings.allow_recurrent_event || settings.allow_multidate_event')
v-icon(v-text='mdiCog' v-bind='attrs' v-on='on')
v-card(outlined :rounded='"0"')
v-card-text
v-row(dense)
v-col(v-if='settings.allow_recurrent_event')
v-switch.mt-0(v-model='show_recurrent' @change="v => setFilter(['show_recurrent', v])"
hide-details :label="$t('event.show_recurrent')" inset)
v-col(v-if='settings.allow_multidate_event')
v-switch.mt-0(v-model='show_multidate' @change="v => setFilter(['show_multidate', v])"
hide-details :label="$t('event.show_multidate')" inset)
v-row(v-if='!showCalendar')
v-col
Calendar.mt-2
v-text-field(slot='placeholder' outlined hide-details :placeholder="$t('common.search')" :append-icon='mdiCog')
span(v-if='showCollectionsBar')
v-btn.mr-2.mt-2(small outlined v-for='collection in collections'
color='primary' :key='collection.id'
:to='`/collection/${encodeURIComponent(collection.name)}`') {{collection.name}}
Calendar.mt-2(v-if='showCalendar')
</template>
<script>
import { mapState } from 'vuex'
import { mapState, mapActions } from 'vuex'
import Calendar from '@/components/Calendar'
import { mdiMagnify, mdiClose } from '@mdi/js'
import { mdiClose, mdiCog } from '@mdi/js'
export default {
data: () => ({
mdiMagnify, mdiClose,
collections: []
data: ({ $store }) => ({
oldRoute: '',
mdiClose, mdiCog,
collections: [],
show_recurrent: $store.state.settings.recurrent_event_visible,
show_multidate: true,
query: ''
}),
async fetch () {
this.collections = await this.$axios.$get('collections').catch(_e => [])
@ -23,21 +55,27 @@ export default {
showSearchBar () {
return this.$route.name === 'index'
},
showCollectionsBar () {
return ['index', 'collection-collection'].includes(this.$route.name)
showCalendar () {
return (!this.settings.hide_calendar && this.$route.name === 'index')
},
...mapState(['settings'])
showCollectionsBar () {
const show = ['index', 'collection-collection'].includes(this.$route.name)
if (show && this.oldRoute !== this.$route.name) {
this.oldRoute = this.$route.name
this.$fetch()
}
return show
},
...mapState(['settings', 'filter'])
},
methods: {
search (ev) {
this.$root.$emit('search', ev)
}
...mapActions(['setFilter']),
}
}
</script>
<style>
#navsearch {
margin: 0 auto;
max-width: 800px;
max-width: 700px;
}
</style>

View file

@ -22,16 +22,19 @@ v-row.mb-4
v-list-item-title(v-text='item.name')
v-list-item-subtitle(v-text='item.address')
//- v-text-field(
//- ref='address'
//- :prepend-icon='mdiMap'
//- :disabled='disableAddress'
//- :rules="[ v => disableAddress ? true : $validators.required('common.address')(v)]"
//- :label="$t('common.address')"
//- @change="changeAddress"
//- :value="value.address")
v-col(cols=12 md=6)
v-combobox(ref='address'
v-text-field(v-if="!settings.allow_geolocation"
ref='address'
:prepend-icon='mdiMap'
:disabled='disableAddress'
:rules="[ v => disableAddress ? true : $validators.required('common.address')(v)]"
:label="$t('common.address')"
:hint="$t('event.address_description')"
persistent-hint
@change="changeAddress"
:value="value.address")
v-combobox(ref='address' v-else
:prepend-icon='mdiMapSearch'
:disabled='disableAddress'
@input.native='searchAddress'
@ -44,7 +47,7 @@ v-row.mb-4
@change='selectAddress'
@focus='searchAddress'
:items="addressList"
:hint="$t('event.address_description' + (settings.allow_geolocation && '_osm'))")
:hint="$t('event.address_description_osm')")
template(v-slot:message="{message, key}")
span(v-html='message' :key="key")
template(v-slot:item="{ item, attrs, on }")
@ -76,7 +79,7 @@ export default {
props: {
value: { type: Object, default: () => ({}) }
},
data () {
data ( {$store} ) {
return {
mdiMap, mdiMapMarker, mdiPlus, mdiMapSearch, mdiLatitude, mdiLongitude, mdiRoadVariant, mdiHome, mdiCityVariant,
place: { },
@ -91,7 +94,14 @@ export default {
node: mdiMapMarker,
relation: mdiCityVariant,
},
nominatim_class: ['amenity', 'shop', 'tourism', 'leisure', 'building']
nominatim_class: ['amenity', 'shop', 'tourism', 'leisure', 'building'],
photon_osm_key: ['amenity', 'shop', 'tourism', 'leisure', 'building'],
photon_osm_type: {
'W': mdiRoadVariant,
'N': mdiMapMarker,
'R': mdiCityVariant,
},
geocoding_provider_type: $store.state.settings.geocoding_provider_type || 'Nominatim'
}
},
computed: {
@ -115,21 +125,33 @@ export default {
return matches
}
},
mounted () {
this.$nextTick( () => {
this.search()
})
},
methods: {
search: debounce(async function(ev) {
const search = ev.target.value.trim().toLowerCase()
const search = ev ? ev.target.value.trim().toLowerCase() : ''
this.places = await this.$axios.$get(`place?search=${search}`)
if (!search && this.places.length) { return this.places }
const matches = this.places.find(p => search === p.name.toLocaleLowerCase())
if (!matches && search) {
this.places.unshift({ create: true, name: ev.target.value.trim() })
}
}, 100),
}, 200),
loadCoordinatesResultIcon(item) {
if (this.geocoding_provider_type == "Nominatim") {
if ( this.nominatim_class.includes(item.class)) {
return this.mdiHome
}
return this.nominatim_osm_type[item.type]
} else if (this.geocoding_provider_type == "Photon") {
if ( this.photon_osm_key.includes(item.class)) {
return this.mdiHome
}
return this.photon_osm_type[item.type]
}
},
selectPlace (p) {
if (!p) { return }
@ -168,11 +190,11 @@ export default {
}
this.$emit('input', { ...this.place })
},
// changeAddress (v) {
// this.place.address = v
// this.$emit('input', { ...this.place })
// this.disableDetails = false
// },
changeAddress (v) {
this.place.address = v
this.$emit('input', { ...this.place })
this.disableDetails = false
},
selectAddress (v) {
if (!v) { return }
if (typeof v === 'object') {
@ -220,7 +242,8 @@ export default {
if (searchCoordinates.length) {
this.loading = true
const ret = await this.$axios.$get(`placeNominatim/${searchCoordinates}`)
const ret = await this.$axios.$get(`placeOSM/${this.geocoding_provider_type}/${searchCoordinates}`)
if (this.geocoding_provider_type == "Nominatim") {
if (ret && ret.length) {
this.addressList = ret.map(v => {
const name = get(v.namedetails, 'alt_name', get(v.namedetails, 'name'))
@ -237,6 +260,38 @@ export default {
} else {
this.addressList = []
}
} else if (this.geocoding_provider_type == "Photon") {
let photon_properties = ['housenumber', 'street', 'locality', 'district', 'city', 'county', 'state', 'postcode', 'country']
if (ret) {
this.addressList = ret.features.map(v => {
let pre_name = v.properties.name || v.properties.street || ''
let pre_address = ''
photon_properties.forEach((item, i) => {
let last = i == (photon_properties.length - 1)
if (v.properties[item] && !last) {
pre_address += v.properties[item]+', '
} else if (v.properties[item]) {
pre_address += v.properties[item]
}
});
let name = pre_name
let address = pre_address
return {
class: v.properties.osm_key,
type: v.properties.osm_type,
lat: v.geometry.coordinates[1],
lon: v.geometry.coordinates[0],
name,
address
}
})
} else {
this.addressList = []
}
}
this.loading = false
}
}, 1000)

View file

@ -33,7 +33,7 @@ v-container
:prepend-icon="mdiTagMultiple"
chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint
:disabled="!collection.id"
placeholder='Tutte'
placeholder='All'
@input.native='searchTags'
@focus='searchTags'
:delimiters="[',', ';']"
@ -69,7 +69,7 @@ v-container
//- v-list-item-subtitle(v-text='item.address')
v-col(cols=2)
v-btn(color='primary' text @click='addFilter' :disabled='!collection.id || !filterPlaces.length && !filterTags.length') add <v-icon v-text='mdiPlus'></v-icon>
v-btn(color='primary' :loading='loading' text @click='addFilter' :disabled='loading || !collection.id || !filterPlaces.length && !filterTags.length') add <v-icon v-text='mdiPlus'></v-icon>
v-data-table(
:headers='filterHeaders'
@ -110,6 +110,9 @@ v-container
<script>
import get from 'lodash/get'
import debounce from 'lodash/debounce'
import isEqual from 'lodash/isEqual'
import sortBy from 'lodash/sortBy'
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiPlus, mdiTagMultiple, mdiMapMarker, mdiDeleteForever, mdiCloseCircle, mdiChevronDown } from '@mdi/js'
export default {
@ -147,7 +150,7 @@ export default {
methods: {
searchTags: debounce(async function (ev) {
this.tags = await this.$axios.$get(`/tag?search=${ev.target.value}`)
this.tags = await this.$axios.$get(`/tag?search=${encodeURIComponent(ev.target.value)}`)
}, 100),
searchPlaces: debounce(async function (ev) {
this.places = await this.$axios.$get(`/place?search=${ev.target.value}`)
@ -163,9 +166,20 @@ export default {
this.loading = true
const tags = this.filterTags
const places = this.filterPlaces.map(p => ({ id: p.id, name: p.name }))
const filter = await this.$axios.$post('/filter', { collectionId: this.collection.id, tags, places })
const filter = { collectionId: this.collection.id, tags, places }
// tags and places are JSON field and there's no way to use them inside a unique constrain
//
const alreadyExists = this.filters.find(f =>
isEqual(sortBy(f.places, 'id'), sortBy(filter.places, 'id')) && isEqual(sortBy(f.tags), sortBy(filter.tags))
)
if (alreadyExists) return
const ret = await this.$axios.$post('/filter', filter )
this.$fetch()
this.filters.push(filter)
this.filters.push(ret)
this.filterTags = []
this.filterPlaces = []
this.loading = false

View file

@ -0,0 +1,169 @@
<template lang="pug">
v-card
v-card-title {{$t('admin.geolocation')}}
v-card-text
p.mb-6(v-html="$t('admin.geolocation_description')")
v-form
v-row
v-col(md=3)
v-autocomplete.mb-4(v-model='geocoding_provider_type'
@blur="save('geocoding_provider_type', geocoding_provider_type )"
:label="$t('admin.geocoding_provider_type')"
:hint="$t('admin.geocoding_provider_type_help')"
persistent-hint
:items="geocoding_provider_type_items"
:placeholder="geocoding_provider_type_default")
v-col(md=5)
v-text-field.mb-4(v-model='geocoding_provider'
@blur="save('geocoding_provider', geocoding_provider )"
:label="$t('admin.geocoding_provider')"
:hint="$t('admin.geocoding_provider_help')"
persistent-hint
:placeholder="geocoding_provider_default")
v-col(md=4)
v-autocomplete.mb-6(v-model="geocoding_countrycodes" :disabled="!(geocoding_provider_type === null || geocoding_provider_type === 'Nominatim')"
:append-icon='mdiChevronDown'
@blur="save('geocoding_countrycodes', geocoding_countrycodes )"
:label="$t('admin.geocoding_countrycodes')"
:items="countries"
multiple chips small-chips persistent-hint
item-value="code"
item-text="name"
:hint="$t('admin.geocoding_countrycodes_help')")
v-row
v-col(md=6)
v-text-field.mb-4(v-model='tilelayer_provider'
@blur="save('tilelayer_provider', tilelayer_provider )"
:label="$t('admin.tilelayer_provider')"
:hint="$t('admin.tilelayer_provider_help')"
persistent-hint
:placeholder="tilelayer_provider_default")
v-col(md=6)
v-text-field(v-model='tilelayer_provider_attribution'
@blur="save('tilelayer_provider_attribution', tilelayer_provider_attribution )"
:label="$t('admin.tilelayer_provider_attribution')"
:placeholder="tilelayer_provider_attribution_default")
div(id="leaflet-map-preview" max-height='10px')
//- Map
v-card-actions
v-spacer
v-btn(color='primary' @click='testGeocodingProvider' :loading='testGeocodingLoading' outlined ) {{$t('admin.geocoding_test_button')}}
v-btn(color='primary' @click='testTileLayerProvider' :loading='testTileLayerLoading' outlined ) {{$t('admin.tilelayer_test_button')}}
</template>
<script>
import { mapActions, mapState } from 'vuex'
import { isoCountries } from '../../server/helpers/geolocation'
import { mdiChevronDown } from '@mdi/js'
// import Map from '~/components/Map'
import "leaflet/dist/leaflet.css"
export default {
props: {
setup: { type: Boolean, default: false }
},
// components: { Map },
data ({ $store }) {
return {
mdiChevronDown,
loading: false,
testGeocodingLoading: false,
testTileLayerLoading: false,
geocoding_provider_type_items: ['Nominatim', 'Photon'],
geocoding_provider_type: $store.state.settings.geocoding_provider_type || '',
geocoding_provider_type_default: 'Nominatim',
geocoding_provider: $store.state.settings.geocoding_provider || '',
geocoding_provider_default: "https://nominatim.openstreetmap.org/search" ,
geocoding_countrycodes: $store.state.settings.geocoding_countrycodes || [],
tilelayer_provider: $store.state.settings.tilelayer_provider || '',
tilelayer_provider_default: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
tilelayer_provider_attribution: $store.state.settings.tilelayer_provider_attribution || '',
tilelayer_provider_attribution_default: '<a target=\'_blank\' href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors',
countries: isoCountries,
mapPreviewTest: null,
}
},
created() {
if (process.client) {
const L = require('leaflet')
}
},
computed: mapState(['settings', 'events']),
methods: {
...mapActions(['setSetting']),
async testGeocodingProvider () {
this.testGeocodingLoading = true
const geocodingProviderTest = this.geocoding_provider || this.geocoding_provider_default
const geocodingSoftwareTest = this.geocoding_provider_type || this.geocoding_provider_type_default
const geocodingQuery = 'building'
try {
if (geocodingSoftwareTest === 'Nominatim') {
const geolocation = await this.$axios.$get(`${geocodingProviderTest}`, {timeout: 3000, params: {q: `${geocodingQuery}`, format: 'json', limit: 1 }} )
} else if (geocodingSoftwareTest === 'Photon') {
const geolocation = await this.$axios.$get(`${geocodingProviderTest}`, {timeout: 3000, params: {q: `${geocodingQuery}`, limit: 1}} )
}
this.$root.$message(this.$t('admin.geocoding_test_success', { service_name: geocodingProviderTest }), { color: 'success' })
} catch (e) {
this.$root.$message(this.$t('admin.tilelayer_test_error', { service_name: geocodingProviderTest }), { color: 'error' })
}
this.testGeocodingLoading = false
},
async testTileLayerProvider () {
this.testTileLayerLoading = true
const tileThis = this
const tileLayerTest = this.tilelayer_provider || this.tilelayer_provider_default
const tileLayerAttributionTest = this.tilelayer_provider_attribution || this.tilelayer_provider_attribution_default
// init tilelayer
if (this.mapPreviewTest == null) {
this.mapPreviewTest = L.map("leaflet-map-preview").setView([40,40],10);
}
this.tileLayer = L.tileLayer(`${tileLayerTest}`, {attribution: `${tileLayerAttributionTest}`})
this.tileLayer.addTo(this.mapPreviewTest)
// tilelayer events inherited from gridlayer https://leafletjs.com/reference.html#gridlayer
this.tileLayer.on('tileload', function (event) {
tileThis.tileLayerTestSucess(event, tileLayerTest)
});
this.tileLayer.on('tileerror', function(error, tile) {
tileThis.tileLayerTestError(event, tileLayerTest)
tileThis.tileLayer = null
});
this.testTileLayerLoading = false
},
save (key, value) {
if (this.settings[key] !== value) {
this.setSetting({ key, value })
}
},
done () {
this.$emit('close')
},
geocodingTestError(event, tileLayerTest) {
this.$root.$message(this.$t('admin.geocoding_test_error', { service_name: geocodingTest }), { color: 'error' })
},
tileLayerTestSucess(event, tileLayerTest) {
this.$root.$message(this.$t('admin.tilelayer_test_success', { service_name: tileLayerTest }), { color: 'success' })
},
tileLayerTestError(event, tileLayerTest) {
this.$root.$message(this.$t('admin.tilelayer_test_error', { service_name: tileLayerTest }), { color: 'error' })
}
}
}
</script>
<style>
#leaflet-map-preview {
height: 20rem;
}
</style>

View file

@ -68,7 +68,7 @@ import debounce from 'lodash/debounce'
import get from 'lodash/get'
export default {
data() {
data( {$store} ) {
return {
mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiEye, mdiMapSearch, mdiChevronDown,
loading: false,
@ -84,11 +84,12 @@ export default {
{ value: 'address', text: this.$t('common.address') },
{ value: 'map', text: 'Map' },
{ value: 'actions', text: this.$t('common.actions'), align: 'right' }
]
],
geocoding_provider_type: $store.state.settings.geocoding_provider_type || 'Nominatim'
}
},
async fetch() {
this.places = await this.$axios.$get('/place/all')
this.places = await this.$axios.$get('/places')
},
computed: {
...mapState(['settings']),
@ -159,12 +160,15 @@ export default {
if (searchCoordinates.length) {
this.loading = true
const ret = await this.$axios.$get(`placeNominatim/${searchCoordinates}`)
const ret = await this.$axios.$get(`placeOSM/${this.geocoding_provider_type}/${searchCoordinates}`)
if (this.geocoding_provider_type == "Nominatim") {
if (ret && ret.length) {
this.addressList = ret.map(v => {
const name = get(v.namedetails, 'alt_name', get(v.namedetails, 'name'))
const address = v.display_name ? v.display_name.replace(name, '').replace(/^, ?/, '') : ''
return {
class: v.class,
type: v.osm_type,
lat: v.lat,
lon: v.lon,
name,
@ -174,6 +178,38 @@ export default {
} else {
this.addressList = []
}
} else if (this.geocoding_provider_type == "Photon") {
let photon_properties = ['housenumber', 'street', 'district', 'city', 'county', 'state', 'postcode', 'country']
if (ret) {
this.addressList = ret.features.map(v => {
let pre_name = v.properties.name || v.properties.street || ''
let pre_address = ''
photon_properties.forEach((item, i) => {
let last = i == (photon_properties.length - 1)
if (v.properties[item] && !last) {
pre_address += v.properties[item]+', '
} else if (v.properties[item]) {
pre_address += v.properties[item]
}
});
let name = pre_name
let address = pre_address
return {
class: v.properties.osm_key,
type: v.properties.osm_type,
lat: v.geometry.coordinates[1],
lon: v.geometry.coordinates[0],
name,
address
}
})
} else {
this.addressList = []
}
}
this.loading = false
}
}, 300)

View file

@ -7,7 +7,9 @@ v-card
v-text-field(v-model='admin_email'
@blur="save('admin_email', admin_email )"
:label="$t('admin.sender_email')"
:label="$t('admin.admin_email')"
:hint="$t('admin.admin_email_help')"
persistent-hint
:rules="$validators.email")
v-switch(v-model='smtp.sendmail'

View file

@ -39,6 +39,10 @@ v-container
inset
:label="$t('admin.allow_anon_event')")
v-switch.mt-1(v-model='allow_multidate_event'
inset
:label="$t('admin.allow_multidate_event')")
v-switch.mt-1(v-model='allow_recurrent_event'
inset
:label="$t('admin.allow_recurrent_event')")
@ -57,32 +61,35 @@ v-container
v-card-actions
v-btn(text @click='showSMTP=true')
<v-icon v-if='!settings.admin_email' color='error' v-text='mdiAlert'></v-icon> {{$t('admin.show_smtp_setup')}}
<v-icon v-if='!settings.admin_email' color='error' class="mr-2" v-text='mdiAlert'></v-icon> {{$t('admin.show_smtp_setup')}}
v-btn(text @click='$emit("complete")' color='primary' v-if='setup') {{$t('common.next')}}
v-icon(v-text='mdiArrowRight')
</template>
<script>
import SMTP from './SMTP.vue'
import Geolocation from './Geolocation.vue'
import { mapActions, mapState } from 'vuex'
import moment from 'dayjs'
import tzNames from './tz.json'
import { mdiAlert, mdiArrowRight } from '@mdi/js'
import { mdiAlert, mdiArrowRight, mdiMap } from '@mdi/js'
const locales = require('../../locales/index')
export default {
props: {
setup: { type: Boolean, default: false }
},
components: { SMTP },
components: { SMTP, Geolocation },
name: 'Settings',
data ({ $store }) {
return {
mdiAlert, mdiArrowRight,
mdiAlert, mdiArrowRight, mdiMap,
title: $store.state.settings.title,
description: $store.state.settings.description,
locales: Object.keys(locales).map(locale => ({ value: locale, text: locales[locale] })),
showSMTP: false,
showGeolocationConfigs: false,
}
},
computed: {
@ -107,6 +114,10 @@ export default {
get () { return this.settings.allow_recurrent_event },
set (value) { this.setSetting({ key: 'allow_recurrent_event', value }) }
},
allow_multidate_event: {
get () { return this.settings.allow_multidate_event },
set (value) { this.setSetting({ key: 'allow_multidate_event', value }) }
},
recurrent_event_visible: {
get () { return this.settings.recurrent_event_visible },
set (value) { this.setSetting({ key: 'recurrent_event_visible', value }) }

114
components/admin/Tags.vue Normal file
View file

@ -0,0 +1,114 @@
<template lang='pug'>
v-container
v-card-title {{ $t('common.tags') }}
v-spacer
v-text-field(v-model='search'
:append-icon='mdiMagnify' outlined rounded
:label="$t('common.search')"
single-line hide-details)
v-dialog(v-model='dialog' width='600' :fullscreen='$vuetify.breakpoint.xsOnly')
v-card
v-card-title {{$t('admin.edit_tag')}} -
strong.ml-2 {{tag.tag}}
v-card-subtitle {{$tc('admin.edit_tag_help', tag.count)}}
v-card-text
v-form(v-model='valid' ref='form' lazy-validation)
v-combobox(v-model='newTag'
:prepend-icon="mdiTag"
hide-no-data
persistent-hint
:items="tags"
:return-object='false'
item-value='tag'
item-text='tag'
:label="$t('common.tags')")
template(v-slot:item="{ item, on, attrs }")
span "{{item.tag}}" <small>({{item.count}})</small>
v-card-actions
v-spacer
v-btn(@click='dialog = false' outlined color='warning') {{ $t('common.cancel') }}
v-btn(@click='saveTag' color='primary' outlined :loading='loading'
:disable='!valid || loading') {{ $t('common.save') }}
v-card-text
v-data-table(
:headers='headers'
:items='tags'
:hide-default-footer='tags.length < 5'
:header-props='{ sortIcon: mdiChevronDown }'
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
:search='search')
template(v-slot:item.map='{ item }')
span {{item.latitude && item.longitude && 'YEP' }}
template(v-slot:item.actions='{ item }')
v-btn(@click='editTag(item)' color='primary' icon)
v-icon(v-text='mdiPencil')
nuxt-link(:to='`/tag/${item.tag}`')
v-icon(v-text='mdiEye')
v-btn(@click='removeTag(item)' color='primary' icon)
v-icon(v-text='mdiDeleteForever')
</template>
<script>
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiEye, mdiMapSearch, mdiChevronDown, mdiDeleteForever, mdiTag } from '@mdi/js'
import { mapState } from 'vuex'
import get from 'lodash/get'
export default {
data() {
return {
mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiEye, mdiMapSearch, mdiChevronDown, mdiDeleteForever, mdiTag,
loading: false,
dialog: false,
valid: false,
tag: {},
newTag: '',
tags: [],
search: '',
headers: [
{ value: 'tag', text: this.$t('common.tag') },
{ value: 'count', text: 'N.' },
{ value: 'actions', text: this.$t('common.actions'), align: 'right' }
]
}
},
async fetch() {
this.tags = await this.$axios.$get('/tags')
},
computed: {
...mapState(['settings']),
},
methods: {
editTag(item) {
this.tag.tag = item.tag
this.tag.count = item.count
this.dialog = true
},
async saveTag() {
if (!this.$refs.form.validate()) return
this.loading = true
this.$nextTick( async () => {
await this.$axios.$put('/tag', { tag: this.tag.tag, newTag: this.newTag })
await this.$fetch()
this.newTag = ''
this.loading = false
this.dialog = false
})
},
async removeTag(tag) {
const ret = await this.$root.$confirm('admin.delete_tag_confirm', { tag: tag.tag, n: tag.count })
if (!ret) { return }
try {
await this.$axios.$delete(`/tag/${encodeURIComponent(tag.tag)}`)
await this.$fetch()
} catch (e) {
const err = get(e, 'response.data.errors[0].message', e)
this.$root.$message(this.$t(err), { color: 'error' })
this.loading = false
}
}
}
}
</script>

View file

@ -16,6 +16,7 @@ v-container
:label="$t('admin.hide_calendar')")
v-card-title {{$t('admin.default_images')}}
v-card-subtitle(v-html="$t('admin.default_images_help')")
v-card-text
v-row
v-col(cols='4')
@ -109,12 +110,13 @@ import { mdiDeleteForever, mdiRestore, mdiPlus, mdiChevronUp } from '@mdi/js'
export default {
name: 'Theme',
data () {
const t = new Date().getMilliseconds()
return {
mdiDeleteForever, mdiRestore, mdiPlus, mdiChevronUp,
valid: false,
logoKey: 0,
fallbackImageKey: 0,
headerImageKey: 0,
logoKey: t,
fallbackImageKey: t,
headerImageKey: t,
link: { href: '', label: '' },
linkModal: false
// menu: [false, false, false, false]

View file

@ -117,9 +117,9 @@ const Ce = /* @__PURE__ */ new Set();
function Ae(t, e) {
t && t.i && (Ce.delete(t), t.i(e));
}
function Me(t, e, i, n) {
const { fragment: l, on_mount: o, on_destroy: r, after_update: f } = t.$$;
l && l.m(e, i), n || Q(() => {
function Me(t, e, i, l) {
const { fragment: n, on_mount: o, on_destroy: r, after_update: f } = t.$$;
n && n.m(e, i), l || Q(() => {
const c = o.map(W).filter(_e);
r ? r.push(...c) : O(c), t.$$.on_mount = [];
}), f.forEach(Q);
@ -131,7 +131,7 @@ function Ne(t, e) {
function Te(t, e) {
t.$$.dirty[0] === -1 && (H.push(t), Se(), t.$$.dirty.fill(0)), t.$$.dirty[e / 31 | 0] |= 1 << e % 31;
}
function ye(t, e, i, n, l, o, r, f = [-1]) {
function ye(t, e, i, l, n, o, r, f = [-1]) {
const c = I;
R(t);
const s = t.$$ = {
@ -139,7 +139,7 @@ function ye(t, e, i, n, l, o, r, f = [-1]) {
ctx: null,
props: o,
update: L,
not_equal: l,
not_equal: n,
bound: ee(),
on_mount: [],
on_destroy: [],
@ -156,8 +156,8 @@ function ye(t, e, i, n, l, o, r, f = [-1]) {
let k = !1;
if (s.ctx = i ? i(t, e.props || {}, (m, _, ...C) => {
const w = C.length ? C[0] : _;
return s.ctx && l(s.ctx[m], s.ctx[m] = w) && (!s.skip_bound && s.bound[m] && s.bound[m](w), k && Te(t, m)), _;
}) : [], s.update(), k = !0, O(s.before_update), s.fragment = n ? n(s.ctx) : !1, e.target) {
return s.ctx && n(s.ctx[m], s.ctx[m] = w) && (!s.skip_bound && s.bound[m] && s.bound[m](w), k && Te(t, m)), _;
}) : [], s.update(), k = !0, O(s.before_update), s.fragment = l ? l(s.ctx) : !1, e.target) {
if (e.hydrate) {
const m = $e(e.target);
s.fragment && s.fragment.l(m), m.forEach(x);
@ -190,8 +190,8 @@ typeof HTMLElement == "function" && (X = class extends HTMLElement {
$on(t, e) {
const i = this.$$.callbacks[t] || (this.$$.callbacks[t] = []);
return i.push(e), () => {
const n = i.indexOf(e);
n !== -1 && i.splice(n, 1);
const l = i.indexOf(e);
l !== -1 && i.splice(l, 1);
};
}
$set(t) {
@ -211,13 +211,13 @@ function F(t, e = "long") {
function V(t) {
return t.multidate ? F(t.start_datetime) + " - " + F(t.end_datetime) : F(t.start_datetime) + (t.end_datetime ? "-" + F(t.end_datetime, "short") : "");
}
function ne(t, e, i) {
const n = t.slice();
return n[12] = e[i], n;
}
function le(t, e, i) {
const n = t.slice();
return n[15] = e[i], n;
const l = t.slice();
return l[12] = e[i], l;
}
function ne(t, e, i) {
const l = t.slice();
return l[15] = e[i], l;
}
function re(t) {
let e;
@ -225,11 +225,11 @@ function re(t) {
c() {
e = g("link"), a(e, "rel", "stylesheet"), a(e, "href", t[4]);
},
m(i, n) {
v(i, e, n);
m(i, l) {
v(i, e, l);
},
p(i, n) {
n & 16 && a(e, "href", i[4]);
p(i, l) {
l & 16 && a(e, "href", i[4]);
},
d(i) {
i && x(e);
@ -237,51 +237,51 @@ function re(t) {
};
}
function oe(t) {
let e, i, n = t[1] && t[3] === "true" && ae(t), l = t[5], o = [];
for (let r = 0; r < l.length; r += 1)
o[r] = ue(ne(t, l, r));
let e, i, l = t[1] && t[3] === "true" && ae(t), n = t[5], o = [];
for (let r = 0; r < n.length; r += 1)
o[r] = ue(le(t, n, r));
return {
c() {
e = g("div"), n && n.c(), i = z();
e = g("div"), l && l.c(), i = z();
for (let r = 0; r < o.length; r += 1)
o[r].c();
a(e, "id", "gancioEvents"), T(e, "dark", t[2] === "dark"), T(e, "light", t[2] === "light"), T(e, "sidebar", t[3] === "true"), T(e, "nosidebar", t[3] !== "true");
},
m(r, f) {
v(r, e, f), n && n.m(e, null), u(e, i);
v(r, e, f), l && l.m(e, null), u(e, i);
for (let c = 0; c < o.length; c += 1)
o[c].m(e, null);
},
p(r, f) {
if (r[1] && r[3] === "true" ? n ? n.p(r, f) : (n = ae(r), n.c(), n.m(e, i)) : n && (n.d(1), n = null), f & 41) {
l = r[5];
if (r[1] && r[3] === "true" ? l ? l.p(r, f) : (l = ae(r), l.c(), l.m(e, i)) : l && (l.d(1), l = null), f & 41) {
n = r[5];
let c;
for (c = 0; c < l.length; c += 1) {
const s = ne(r, l, c);
for (c = 0; c < n.length; c += 1) {
const s = le(r, n, c);
o[c] ? o[c].p(s, f) : (o[c] = ue(s), o[c].c(), o[c].m(e, null));
}
for (; c < o.length; c += 1)
o[c].d(1);
o.length = l.length;
o.length = n.length;
}
f & 4 && T(e, "dark", r[2] === "dark"), f & 4 && T(e, "light", r[2] === "light"), f & 8 && T(e, "sidebar", r[3] === "true"), f & 8 && T(e, "nosidebar", r[3] !== "true");
},
d(r) {
r && x(e), n && n.d(), pe(o, r);
r && x(e), l && l.d(), pe(o, r);
}
};
}
function ae(t) {
let e, i, n, l, o, r, f;
let e, i, l, n, o, r, f;
return {
c() {
e = g("a"), i = g("div"), n = g("div"), l = j(t[1]), o = z(), r = g("img"), a(n, "class", "title"), a(r, "id", "logo"), a(r, "alt", "logo"), G(r.src, f = t[0] + "/logo.png") || a(r, "src", f), a(i, "class", "content"), a(e, "href", t[0]), a(e, "target", "_blank"), a(e, "id", "header");
e = g("a"), i = g("div"), l = g("div"), n = j(t[1]), o = z(), r = g("img"), a(l, "class", "title"), a(r, "id", "logo"), a(r, "alt", "logo"), G(r.src, f = t[0] + "/logo.png") || a(r, "src", f), a(i, "class", "content"), a(e, "href", t[0]), a(e, "target", "_blank"), a(e, "id", "header");
},
m(c, s) {
v(c, e, s), u(e, i), u(i, n), u(n, l), u(i, o), u(i, r);
v(c, e, s), u(e, i), u(i, l), u(l, n), u(i, o), u(i, r);
},
p(c, s) {
s & 2 && N(l, c[1]), s & 1 && !G(r.src, f = c[0] + "/logo.png") && a(r, "src", f), s & 1 && a(e, "href", c[0]);
s & 2 && N(n, c[1]), s & 1 && !G(r.src, f = c[0] + "/logo.png") && a(r, "src", f), s & 1 && a(e, "href", c[0]);
},
d(c) {
c && x(e);
@ -293,50 +293,50 @@ function se(t) {
function i(o, r) {
return o[12].media.length ? Ge : Le;
}
let n = i(t), l = n(t);
let l = i(t), n = l(t);
return {
c() {
e = g("div"), l.c(), a(e, "class", "img");
e = g("div"), n.c(), a(e, "class", "img");
},
m(o, r) {
v(o, e, r), l.m(e, null);
v(o, e, r), n.m(e, null);
},
p(o, r) {
n === (n = i(o)) && l ? l.p(o, r) : (l.d(1), l = n(o), l && (l.c(), l.m(e, null)));
l === (l = i(o)) && n ? n.p(o, r) : (n.d(1), n = l(o), n && (n.c(), n.m(e, null)));
},
d(o) {
o && x(e), l.d();
o && x(e), n.d();
}
};
}
function Le(t) {
let e, i, n;
let e, i, l;
return {
c() {
e = g("img"), a(e, "style", "aspect-ratio=1.7778;"), a(e, "alt", i = t[12].title), G(e.src, n = t[0] + "/fallbackimage.png") || a(e, "src", n), a(e, "loading", "lazy");
e = g("img"), a(e, "style", "aspect-ratio=1.7778;"), a(e, "alt", i = t[12].title), G(e.src, l = t[0] + "/fallbackimage.png") || a(e, "src", l), a(e, "loading", "lazy");
},
m(l, o) {
v(l, e, o);
m(n, o) {
v(n, e, o);
},
p(l, o) {
o & 32 && i !== (i = l[12].title) && a(e, "alt", i), o & 1 && !G(e.src, n = l[0] + "/fallbackimage.png") && a(e, "src", n);
p(n, o) {
o & 32 && i !== (i = n[12].title) && a(e, "alt", i), o & 1 && !G(e.src, l = n[0] + "/fallbackimage.png") && a(e, "src", l);
},
d(l) {
l && x(e);
d(n) {
n && x(e);
}
};
}
function Ge(t) {
let e, i, n, l;
let e, i, l, n;
return {
c() {
e = g("img"), a(e, "style", i = "object-position: " + de(t[12]) + "; aspect-ratio=1.7778;"), a(e, "alt", n = t[12].media[0].name), G(e.src, l = t[0] + "/media/thumb/" + t[12].media[0].url) || a(e, "src", l), a(e, "loading", "lazy");
e = g("img"), a(e, "style", i = "object-position: " + de(t[12]) + "; aspect-ratio=1.7778;"), a(e, "alt", l = t[12].media[0].name), G(e.src, n = t[0] + "/media/thumb/" + t[12].media[0].url) || a(e, "src", n), a(e, "loading", "lazy");
},
m(o, r) {
v(o, e, r);
},
p(o, r) {
r & 32 && i !== (i = "object-position: " + de(o[12]) + "; aspect-ratio=1.7778;") && a(e, "style", i), r & 32 && n !== (n = o[12].media[0].name) && a(e, "alt", n), r & 33 && !G(e.src, l = o[0] + "/media/thumb/" + o[12].media[0].url) && a(e, "src", l);
r & 32 && i !== (i = "object-position: " + de(o[12]) + "; aspect-ratio=1.7778;") && a(e, "style", i), r & 32 && l !== (l = o[12].media[0].name) && a(e, "alt", l), r & 33 && !G(e.src, n = o[0] + "/media/thumb/" + o[12].media[0].url) && a(e, "src", n);
},
d(o) {
o && x(e);
@ -344,50 +344,50 @@ function Ge(t) {
};
}
function ce(t) {
let e, i = t[12].tags, n = [];
for (let l = 0; l < i.length; l += 1)
n[l] = fe(le(t, i, l));
let e, i = t[12].tags, l = [];
for (let n = 0; n < i.length; n += 1)
l[n] = fe(ne(t, i, n));
return {
c() {
e = g("div");
for (let l = 0; l < n.length; l += 1)
n[l].c();
for (let n = 0; n < l.length; n += 1)
l[n].c();
a(e, "class", "tags");
},
m(l, o) {
v(l, e, o);
for (let r = 0; r < n.length; r += 1)
n[r].m(e, null);
m(n, o) {
v(n, e, o);
for (let r = 0; r < l.length; r += 1)
l[r].m(e, null);
},
p(l, o) {
p(n, o) {
if (o & 32) {
i = l[12].tags;
i = n[12].tags;
let r;
for (r = 0; r < i.length; r += 1) {
const f = le(l, i, r);
n[r] ? n[r].p(f, o) : (n[r] = fe(f), n[r].c(), n[r].m(e, null));
const f = ne(n, i, r);
l[r] ? l[r].p(f, o) : (l[r] = fe(f), l[r].c(), l[r].m(e, null));
}
for (; r < n.length; r += 1)
n[r].d(1);
n.length = i.length;
for (; r < l.length; r += 1)
l[r].d(1);
l.length = i.length;
}
},
d(l) {
l && x(e), pe(n, l);
d(n) {
n && x(e), pe(l, n);
}
};
}
function fe(t) {
let e, i, n = t[15] + "", l;
let e, i, l = t[15] + "", n;
return {
c() {
e = g("span"), i = j("#"), l = j(n), a(e, "class", "tag");
e = g("span"), i = j("#"), n = j(l), a(e, "class", "tag");
},
m(o, r) {
v(o, e, r), u(e, i), u(e, l);
v(o, e, r), u(e, i), u(e, n);
},
p(o, r) {
r & 32 && n !== (n = o[15] + "") && N(l, n);
r & 32 && l !== (l = o[15] + "") && N(n, l);
},
d(o) {
o && x(e);
@ -395,16 +395,16 @@ function fe(t) {
};
}
function ue(t) {
let e, i, n, l, o = V(t[12]) + "", r, f, c, s = t[12].title + "", k, m, _, C, w = t[12].place.name + "", d, S, h, b = t[12].place.address + "", A, Y, Z, U, q, $ = t[3] !== "true" && se(t), E = t[12].tags.length && ce(t);
let e, i, l, n, o = V(t[12]) + "", r, f, c, s = t[12].title + "", k, m, _, C, w = t[12].place.name + "", d, S, h, b = t[12].place.address + "", A, Y, Z, U, q, $ = t[3] !== "true" && se(t), E = t[12].tags.length && ce(t);
return {
c() {
e = g("a"), $ && $.c(), i = z(), n = g("div"), l = g("div"), r = j(o), f = z(), c = g("div"), k = j(s), m = z(), _ = g("span"), C = j("@"), d = j(w), S = z(), h = g("span"), A = j(b), Y = z(), E && E.c(), Z = z(), a(l, "class", "subtitle"), a(c, "class", "title"), a(h, "class", "subtitle"), a(_, "class", "place"), a(n, "class", "content"), a(e, "href", U = t[0] + "/event/" + (t[12].slug || t[12].id)), a(e, "class", "event"), a(e, "title", q = t[12].title), a(e, "target", "_blank");
e = g("a"), $ && $.c(), i = z(), l = g("div"), n = g("div"), r = j(o), f = z(), c = g("div"), k = j(s), m = z(), _ = g("span"), C = j("@"), d = j(w), S = z(), h = g("span"), A = j(b), Y = z(), E && E.c(), Z = z(), a(n, "class", "subtitle"), a(c, "class", "title"), a(h, "class", "subtitle"), a(_, "class", "place"), a(l, "class", "content"), a(e, "href", U = t[0] + "/event/" + (t[12].slug || t[12].id)), a(e, "class", "event"), a(e, "title", q = t[12].title), a(e, "target", "_blank");
},
m(p, M) {
v(p, e, M), $ && $.m(e, null), u(e, i), u(e, n), u(n, l), u(l, r), u(n, f), u(n, c), u(c, k), u(n, m), u(n, _), u(_, C), u(_, d), u(_, S), u(_, h), u(h, A), u(n, Y), E && E.m(n, null), u(e, Z);
v(p, e, M), $ && $.m(e, null), u(e, i), u(e, l), u(l, n), u(n, r), u(l, f), u(l, c), u(c, k), u(l, m), u(l, _), u(_, C), u(_, d), u(_, S), u(_, h), u(h, A), u(l, Y), E && E.m(l, null), u(e, Z);
},
p(p, M) {
p[3] !== "true" ? $ ? $.p(p, M) : ($ = se(p), $.c(), $.m(e, i)) : $ && ($.d(1), $ = null), M & 32 && o !== (o = V(p[12]) + "") && N(r, o), M & 32 && s !== (s = p[12].title + "") && N(k, s), M & 32 && w !== (w = p[12].place.name + "") && N(d, w), M & 32 && b !== (b = p[12].place.address + "") && N(A, b), p[12].tags.length ? E ? E.p(p, M) : (E = ce(p), E.c(), E.m(n, null)) : E && (E.d(1), E = null), M & 33 && U !== (U = p[0] + "/event/" + (p[12].slug || p[12].id)) && a(e, "href", U), M & 32 && q !== (q = p[12].title) && a(e, "title", q);
p[3] !== "true" ? $ ? $.p(p, M) : ($ = se(p), $.c(), $.m(e, i)) : $ && ($.d(1), $ = null), M & 32 && o !== (o = V(p[12]) + "") && N(r, o), M & 32 && s !== (s = p[12].title + "") && N(k, s), M & 32 && w !== (w = p[12].place.name + "") && N(d, w), M & 32 && b !== (b = p[12].place.address + "") && N(A, b), p[12].tags.length ? E ? E.p(p, M) : (E = ce(p), E.c(), E.m(l, null)) : E && (E.d(1), E = null), M & 33 && U !== (U = p[0] + "/event/" + (p[12].slug || p[12].id)) && a(e, "href", U), M & 32 && q !== (q = p[12].title) && a(e, "title", q);
},
d(p) {
p && x(e), $ && $.d(), E && E.d();
@ -412,21 +412,21 @@ function ue(t) {
};
}
function He(t) {
let e, i, n = t[4] && re(t), l = t[5].length && oe(t);
let e, i, l = t[4] && re(t), n = t[5].length && oe(t);
return {
c() {
n && n.c(), e = z(), l && l.c(), i = ve(), this.c = L;
l && l.c(), e = z(), n && n.c(), i = ve(), this.c = L;
},
m(o, r) {
n && n.m(o, r), v(o, e, r), l && l.m(o, r), v(o, i, r);
l && l.m(o, r), v(o, e, r), n && n.m(o, r), v(o, i, r);
},
p(o, [r]) {
o[4] ? n ? n.p(o, r) : (n = re(o), n.c(), n.m(e.parentNode, e)) : n && (n.d(1), n = null), o[5].length ? l ? l.p(o, r) : (l = oe(o), l.c(), l.m(i.parentNode, i)) : l && (l.d(1), l = null);
o[4] ? l ? l.p(o, r) : (l = re(o), l.c(), l.m(e.parentNode, e)) : l && (l.d(1), l = null), o[5].length ? n ? n.p(o, r) : (n = oe(o), n.c(), n.m(i.parentNode, i)) : n && (n.d(1), n = null);
},
i: L,
o: L,
d(o) {
n && n.d(o), o && x(e), l && l.d(o), o && x(i);
l && l.d(o), o && x(e), n && n.d(o), o && x(i);
}
};
}
@ -438,12 +438,12 @@ function de(t) {
return "center center";
}
function Re(t, e, i) {
let { baseurl: n = "" } = e, { title: l = "" } = e, { maxlength: o = !1 } = e, { tags: r = "" } = e, { places: f = "" } = e, { theme: c = "light" } = e, { show_recurrent: s = !1 } = e, { sidebar: k = "true" } = e, { external_style: m = "" } = e, _ = !1, C = [];
let { baseurl: l = "" } = e, { title: n = "" } = e, { maxlength: o = !1 } = e, { tags: r = "" } = e, { places: f = "" } = e, { theme: c = "light" } = e, { show_recurrent: s = !1 } = e, { sidebar: k = "true" } = e, { external_style: m = "" } = e, _ = !1, C = [];
function w(d) {
if (!_)
return;
const S = [];
o && S.push(`max=${o}`), r && S.push(`tags=${r}`), f && S.push(`places=${f}`), S.push(`show_recurrent=${s ? "true" : "false"}`), fetch(`${n}/api/events?${S.join("&")}`).then((h) => h.json()).then((h) => {
o && S.push(`max=${o}`), r && S.push(`tags=${r}`), f && S.push(`places=${f}`), S.push(`show_recurrent=${s ? "true" : "false"}`), fetch(`${l}/api/events?${S.join("&")}`).then((h) => h.json()).then((h) => {
i(5, C = h);
}).catch((h) => {
console.error("Error loading Gancio API -> ", h);
@ -452,12 +452,12 @@ function Re(t, e, i) {
return we(() => {
_ = !0, w();
}), t.$$set = (d) => {
"baseurl" in d && i(0, n = d.baseurl), "title" in d && i(1, l = d.title), "maxlength" in d && i(6, o = d.maxlength), "tags" in d && i(7, r = d.tags), "places" in d && i(8, f = d.places), "theme" in d && i(2, c = d.theme), "show_recurrent" in d && i(9, s = d.show_recurrent), "sidebar" in d && i(3, k = d.sidebar), "external_style" in d && i(4, m = d.external_style);
"baseurl" in d && i(0, l = d.baseurl), "title" in d && i(1, n = d.title), "maxlength" in d && i(6, o = d.maxlength), "tags" in d && i(7, r = d.tags), "places" in d && i(8, f = d.places), "theme" in d && i(2, c = d.theme), "show_recurrent" in d && i(9, s = d.show_recurrent), "sidebar" in d && i(3, k = d.sidebar), "external_style" in d && i(4, m = d.external_style);
}, t.$$.update = () => {
t.$$.dirty & 975 && w();
}, [
n,
l,
n,
c,
k,
m,
@ -570,13 +570,13 @@ class Ie extends X {
}
customElements.define("gancio-events", Ie);
function he(t) {
let e, i, n, l, o = t[1].title + "", r, f, c, s = V(t[1]) + "", k, m, _, C, w = t[1].place.name + "", d, S, h = t[1].media.length && ge(t);
let e, i, l, n, o = t[1].title + "", r, f, c, s = V(t[1]) + "", k, m, _, C, w = t[1].place.name + "", d, S, h = t[1].media.length && ge(t);
return {
c() {
e = g("a"), h && h.c(), i = z(), n = g("div"), l = g("strong"), r = j(o), f = z(), c = g("div"), k = j(s), m = z(), _ = g("div"), C = j("@"), d = j(w), a(_, "class", "place"), a(n, "class", "container"), a(e, "href", S = t[0] + "/event/" + (t[1].slug || t[1].id)), a(e, "class", "card"), a(e, "target", "_blank");
e = g("a"), h && h.c(), i = z(), l = g("div"), n = g("strong"), r = j(o), f = z(), c = g("div"), k = j(s), m = z(), _ = g("div"), C = j("@"), d = j(w), a(_, "class", "place"), a(l, "class", "container"), a(e, "href", S = t[0] + "/event/" + (t[1].slug || t[1].id)), a(e, "class", "card"), a(e, "target", "_blank");
},
m(b, A) {
v(b, e, A), h && h.m(e, null), u(e, i), u(e, n), u(n, l), u(l, r), u(n, f), u(n, c), u(c, k), u(n, m), u(n, _), u(_, C), u(_, d);
v(b, e, A), h && h.m(e, null), u(e, i), u(e, l), u(l, n), u(n, r), u(l, f), u(l, c), u(c, k), u(l, m), u(l, _), u(_, C), u(_, d);
},
p(b, A) {
b[1].media.length ? h ? h.p(b, A) : (h = ge(b), h.c(), h.m(e, i)) : h && (h.d(1), h = null), A & 2 && o !== (o = b[1].title + "") && N(r, o), A & 2 && s !== (s = V(b[1]) + "") && N(k, s), A & 2 && w !== (w = b[1].place.name + "") && N(d, w), A & 3 && S !== (S = b[0] + "/event/" + (b[1].slug || b[1].id)) && a(e, "href", S);
@ -587,16 +587,16 @@ function he(t) {
};
}
function ge(t) {
let e, i, n, l;
let e, i, l, n;
return {
c() {
e = g("img"), G(e.src, i = t[2](t[1])) || a(e, "src", i), a(e, "alt", n = t[1].media[0].name), a(e, "style", l = "object-position: " + me(t[1]) + "; aspect-ratio=1.7778;");
e = g("img"), G(e.src, i = t[2](t[1])) || a(e, "src", i), a(e, "alt", l = t[1].media[0].name), a(e, "style", n = "object-position: " + me(t[1]) + "; aspect-ratio=1.7778;");
},
m(o, r) {
v(o, e, r);
},
p(o, r) {
r & 2 && !G(e.src, i = o[2](o[1])) && a(e, "src", i), r & 2 && n !== (n = o[1].media[0].name) && a(e, "alt", n), r & 2 && l !== (l = "object-position: " + me(o[1]) + "; aspect-ratio=1.7778;") && a(e, "style", l);
r & 2 && !G(e.src, i = o[2](o[1])) && a(e, "src", i), r & 2 && l !== (l = o[1].media[0].name) && a(e, "alt", l), r & 2 && n !== (n = "object-position: " + me(o[1]) + "; aspect-ratio=1.7778;") && a(e, "style", n);
},
d(o) {
o && x(e);
@ -609,16 +609,16 @@ function Oe(t) {
c() {
i && i.c(), e = ve(), this.c = L;
},
m(n, l) {
i && i.m(n, l), v(n, e, l);
m(l, n) {
i && i.m(l, n), v(l, e, n);
},
p(n, [l]) {
n[1] ? i ? i.p(n, l) : (i = he(n), i.c(), i.m(e.parentNode, e)) : i && (i.d(1), i = null);
p(l, [n]) {
l[1] ? i ? i.p(l, n) : (i = he(l), i.c(), i.m(e.parentNode, e)) : i && (i.d(1), i = null);
},
i: L,
o: L,
d(n) {
i && i.d(n), n && x(e);
d(l) {
i && i.d(l), l && x(e);
}
};
}
@ -630,21 +630,21 @@ function me(t) {
return "center center";
}
function Ue(t, e, i) {
let { baseurl: n = "https://demo.gancio.org" } = e, { id: l } = e, o = !1, r;
let { baseurl: l = "https://demo.gancio.org" } = e, { id: n } = e, o = !1, r;
function f(s, k) {
o && fetch(`${k}/api/event/${s}`).then((m) => m.json()).then((m) => i(1, r = m));
o && fetch(`${k}/api/event/detail/${s}`).then((m) => m.json()).then((m) => i(1, r = m));
}
we(() => {
o = !0, f(l, n);
o = !0, f(n, l);
});
function c(s) {
return `${n}/media/thumb/${s.media[0].url}`;
return `${l}/media/thumb/${s.media[0].url}`;
}
return t.$$set = (s) => {
"baseurl" in s && i(0, n = s.baseurl), "id" in s && i(3, l = s.id);
"baseurl" in s && i(0, l = s.baseurl), "id" in s && i(3, n = s.id);
}, t.$$.update = () => {
t.$$.dirty & 9 && f(l, n);
}, [n, r, c, l];
t.$$.dirty & 9 && f(n, l);
}, [l, r, c, n];
}
class qe extends X {
constructor(e) {

View file

@ -8,6 +8,47 @@ nav_order: 10
All notable changes to this project will be documented in this file.
### 1.6.2 - 12 jan '23
- add swipe gesture to move to next/prev event
- fix refresh collections, fix #219
- add russian translation (thanks @drunkod)
- refactor search / filter / selection fix #225, 227, #224
- models initialization refactored, better dev experience, fix backend HMR
### 1.6.1 - 15 dec '22
- allow edit tags in admin panel, fix #170
- fix header / fallback image upload, fix #222
- fix WPGancio MU
- fix recurrent events label
- update translations (de, es, eu, gl)
### 1.6.0 - 11 dec '22
- new plugin system - fix #177
- new "publish on telegram" plugin: (thanks @fadelkon)
- people can now choose the language displayed - fix #171
- admin could choose a custom fallback image - fix #195
- it is now possible NOT to enter the end time of an event - fix #188
- live search
- improve event import
- add Apple touch icon - fix #200
- add nominatim / openstreetmap search feature (thanks @sedum)
- new hide calendar option
- new hide thumbs from homepage option
- linkable admin tab
- friendly instances label is now customizable (thanks @sedum)
- i18n refactoring
- Wordpress plugin now supports MU installation
- new chinese translation
- new portuguese translation
- improved navbar layout
- improved event layout
- complete oauth2 refactoring
- fix ics unique uuid
- fix place "[Object]" issue - #194
- fix random restart while downloading random media
- fix mobile dialog layout
- urlencode place and tag urls
### 1.5.6 - 22 set '22
- update linkifyjs, sequelizem, nuxt deps
- improve homepage loading time

View file

@ -11,5 +11,5 @@ nav_order: 9
- :elephant: Mastodon ⇒ [@gancio@mastodon.cisti.org](https://mastodon.cisti.org/@gancio)
- :email: Email ⇒ [info@cisti.org](mailto:info@cisti.org)
- IRC ⇒ #gancio @ irc.autistici.org (sometimes...)
- Issues ⇒ https://framagit.org/les/gancio/-/issues
- Issues ⇒ [https://framagit.org/les/gancio/-/issues](https://framagit.org/les/gancio/-/issues)

View file

@ -0,0 +1,7 @@
NOMINATIM_PASSWORD=CeMA4M1kiDo0k
# Choose PBF_PATH to import a local file
PBF_PATH=/nominatim/data/default.osm.pbf
# PBF_URL= https://download.geofabrik.de/europe/italy/nord-est-latest.osm.pbf
# REPLICATION_URL= https://download.geofabrik.de/europe/italy/nord-est-updates/

View file

@ -0,0 +1,19 @@
version: '3'
services:
nominatim:
container_name: nominatim
image: mediagis/nominatim:4.2
restart: always
ports:
- "8080:8080"
environment:
# see https://github.com/mediagis/nominatim-docker/tree/master/4.2#configuration for more options
PBF_PATH: "${PBF_PATH}"
PBF_URL: "${PBF_URL}"
REPLICATION_URL: "${REPLICATION_URL}"
NOMINATIM_PASSWORD: ${NOMINATIM_PASSWORD}
volumes:
- ./nominatim-data:/var/lib/postgresql/14/main
- ./nominatim/data/"${PBF_PATH}":/nominatim/data/"${PBF_PATH}"
shm_size: 1gb

View file

@ -16,7 +16,7 @@ The configuration file shoud be a `.json` or a `.js` file and could be specified
1. TOC
{:toc}
- ### Server
### Server
This probably support unix socket too
```json
@ -26,7 +26,7 @@ This probably support unix socket too
}
```
- ### Database
### Database
DB configuration, look [here](https://sequelize.org/v6/class/src/sequelize.js~Sequelize.html#instance-constructor-constructor) for options.
```json
"db": {
@ -34,11 +34,15 @@ DB configuration, look [here](https://sequelize.org/v6/class/src/sequelize.js~Se
"storage": "/tmp/db.sqlite"
}
```
- ### Upload path
### Upload path
Where to save images
`"upload_path": "./uploads"`
- ### User locale
### Plugins path
Where to search for [plugins](/usage/plugins)
`"plugins_path": "./plugins"`
### User locale
Probably you want to modify some text for your specific community, that's
why we thought the `user_locale` configuration: you can specify your version of
each string of **gancio** making a directory with your locales inside.

139
docs/install/nominatim.md Normal file
View file

@ -0,0 +1,139 @@
---
layout: default
title: Nominatim
permalink: /install/nominatim
parent: Install
nav_order: 7
---
## Nominatim installation
{: .no_toc }
1. TOC
{:toc}
---
## Testing
For testing purposes you could skip the nominatim installation and use one of this geocoding providers that run a server for free:
- [https://photon.komoot.io/](https://photon.komoot.io/) [Terms of service](https://photon.komoot.io/)
- [https://nominatim.openstreetmap.org/](https://nominatim.openstreetmap.org/) [Terms of service](https://operations.osmfoundation.org/policies/nominatim/)
---
## Requirements
From [https://nominatim.org/release-docs/latest/admin/Installation/](https://nominatim.org/release-docs/latest/admin/Installation/)
"A minimum of 2GB of RAM is required or installation will fail. For a full planet import 128GB of RAM or more are strongly recommended. Do not report out of memory problems if you have less than 64GB RAM."
### Planet mirrors
There is a list of planet mirror at [https://wiki.openstreetmap.org/wiki/Planet.osm#Planet.osm_mirrors](https://wiki.openstreetmap.org/wiki/Planet.osm#Planet.osm_mirrors)
There you can also find `Country and area extracts`, divided by `Worldwide extract sources` and `Regional extract sources`
### Download an extract
For Nominatim to work, you will needs to import files in [PBF Format](https://wiki.openstreetmap.org/wiki/PBF_Format) in the PostGis database. Those files have extension `*.osm.pbf`.
Some of these mirrors provide also incremental updates via [OsmChange](https://wiki.openstreetmap.org/wiki/OsmChange), for example:
- Provides updates but with a lower detail
[https://download.geofabrik.de/europe/italy/nord-ovest-updates/nord-ovest-latest.osm.pbf](http://download.geofabrik.de/europe/italy/nord-ovest-latest.osm.pbf)
[https://download.geofabrik.de/europe/italy/nord-ovest-updates/](https://download.geofabrik.de/europe/italy/nord-ovest-updates/)
- Does not provide updates but as higher level of detail
[https://osmit-estratti-test.wmcloud.org/dati/poly/province/pbf/015_Milano_poly.osm.pbf](https://osmit-estratti-test.wmcloud.org/dati/poly/province/pbf/015_Milano_poly.osm.pbf)
Needs to host multiple areas? Checkout [Osmium](https://osmcode.org/osmium-tool/manual.html), to merge multiple PBF files into one.
---
## Install on Debian
There is a [detailed documentaion](https://nominatim.org/release-docs/latest/appendix/Install-on-Ubuntu-22/) for installing nominatim on `Ubuntu 22` that should be valid also to install on `Debian`.
### Setup
[https://nominatim.org/release-docs/latest/appendix/Install-on-Ubuntu-22/#installing-the-required-software](https://nominatim.org/release-docs/latest/appendix/Install-on-Ubuntu-22/#installing-the-required-software)
### Building and Configuration
Get the source code from Github and change into the source directory
```
cd $USERHOME
wget https://nominatim.org/release/Nominatim-4.2.0.tar.bz2
tar xf Nominatim-4.2.0.tar.bz2
```
The code must be built in a separate directory. Create this directory, then configure and build Nominatim in there:
```
mkdir $USERHOME/build
cd $USERHOME/build
cmake $USERHOME/Nominatim-4.2.0
make
```
### Setting up the webserver
[https://nominatim.org/release-docs/latest/appendix/Install-on-Ubuntu-22/#setting-up-a-webserver](https://nominatim.org/release-docs/latest/appendix/Install-on-Ubuntu-22/#setting-up-a-webserver)
### Import the database
[https://nominatim.org/release-docs/latest/admin/Import/](https://nominatim.org/release-docs/latest/admin/Import/)
---
## Install using docker
### Setup
Make sure to have [Docker Engine](https://docs.docker.com/engine/install/),
[Docker Compose](https://docs.docker.com/compose/install/) and [git](https://git-scm.com/downloads) installed:
```bash
sudo apt install docker docker-compose git
```
### Clone the project
From [https://github.com/mediagis/nominatim-docker](https://github.com/mediagis/nominatim-docker)
- Clone the project from sources
```bash
git clone git@github.com:mediagis/nominatim-docker.git
# cd nominatim-docker/<version>
cd nominatim-docker/4.2/contrib # released Nov 29, 2022
docker-compose pull
```
- Or, use the template at `docs/docker/nominatim`
```
cd /opt/gancio/docs/docker/nominatim
docker-compose pull
```
### Import the database
See [Requirements](#requirements) about downloading the `.osm.pbf` files
```bash
cd docs/docker/nominatim/
wget https://download.geofabrik.de/europe/italy/nord-ovest-latest.osm.pbf \
./nominatim/data/default.osm.pbf
```
### Configure the environment file
```
cd docs/docker/nominatim/
cp .env.example .env
```
Create a random password for nominatim a add it to .env file
```bash
NOMINATIM_PASSWORD=random_password;
NOMINATIM_PASSWORD=$(echo $NOMINATIM_PASSWORD | openssl passwd --stdin);
echo $NOMINATIM_PASSWORD;
sed -i -e 's/\(NOMINATIM_PASSWORD=\)\(.*\)/\1'$NOMINATIM_PASSWORD'/g' .env
```
### Start nominatim-docker
Start your container:
```bash
docker-compose up -d
```
Checkout the logs to see when data are imported to the database:
```bash
docker-compose logs -f
```
Try out the search:
[http://0.0.0.0:8080/search?q=building](http://0.0.0.0:8080/search?q=building)

View file

@ -11,7 +11,11 @@ nav_order: 7
- [sapratza.in](https://sapratza.in/) (Sardinia, Italy)
- [ponente.rocks](https://ponente.rocks) (Ponente Ligure, Italy)
- [puntello.org](https://puntello.org) (Milan, Italy)
- [lasitua.org](https://lasitua.org) (Brescia, Italy)
- [balotta.org](https://balotta.org) (Bologna, Italy)
- [gancio.daghe.xyz](https://gancio.daghe.xyz/) (Trento, Italy)
- [bcn.convoca.la](https://bcn.convoca.la/) (Barcelona)
- [mad.convoca.la](https://bcn.convoca.la/) (Madrid)
- [bonn.jetzt](https://bonn.jetzt/) (Digital-Events aus Bonn, Rhein-Sieg und der Region)
- [quest.livellosegreto.it](https://quest.livellosegreto.it/)
- [ezkerraldea.euskaragendak.eus](https://ezkerraldea.euskaragendak.eus/)
@ -20,6 +24,7 @@ nav_order: 7
- [lubakiagenda.net](https://lubakiagenda.net/)
- [eventos.coletivos.org](https://eventos.coletivos.org/)
- [calendario.extinctionrebellion.es](https://calendario.extinctionrebellion.es/)
- [cloudspeakers.org](https://cloudspeakers.org/) (Utrecht?)
<small>Do you want your instance to appear here? [Write us]({% link contact.md %}).</small>

62
docs/usage/plugins.md Normal file
View file

@ -0,0 +1,62 @@
---
layout: default
title: Plugins
permalink: /usage/plugins
nav_order: 2
parent: Usage
has_toc: true
---
# Plugins
{: .no_toc }
This page is a guide to install plugins, if you want to develop one instead look [here](/dev/plugins)
1. TOC
{:toc}
## Install
To install a plugin you have to:
1. **download the .zip archive (look for the url on the plugin list below)**
```
wget https://framagit.org/les/gancio-plugin-telegram-bridge/-/archive/v0.2.0/gancio-plugin-telegram-bridge-v0.2.0.zip
```
2. **unpack it in the `./plugins` directory.**
```
cd plugins
unzip https://framagit.org/les/gancio-plugin-telegram-bridge/-/archive/v0.2.0/gancio-plugin-telegram-bridge-v0.2.0.zip
```
3. **install the dependencies with `yarn`**
```
cd plugins/gancio-plugin-telegram-bridge
yarn
```
4. **restart gancio**
__with debian__
```
sudo sytemctl restart gancio
```
__with docker__
```
docker-compose restart
```
# List of plugins
## __Telegram__
This plugin republishes events to Telegram channels or groups.
The goal is to spread the info of our networks to the capitalist cyberspace, and pull otherwise isolated people to our radical and free part of the internet.
- **Website**: [https://framagit.org/bcn.convocala/gancio-plugin-telegram-bridge](https://framagit.org/bcn.convocala/gancio-plugin-telegram-bridge)
- **Download**: [gancio-plugin-telegram-bridge-v0.2.0.zip](https://framagit.org/les/gancio-plugin-telegram-bridge/-/archive/v0.2.0/gancio-plugin-telegram-bridge-v0.2.0.zip)
- **Release**: v0.2.0 / 10 Dec '22

View file

@ -4,7 +4,9 @@
<v-main>
<Snackbar/>
<Confirm/>
<nuxt :keep-alive='$route.name === "index"'/>
<v-fade-transition hide-on-leave>
<nuxt />
</v-fade-transition>
</v-main>
<Footer/>

View file

@ -98,7 +98,11 @@
"home": "Inici",
"help_translate": "Ajuda amb la traducció",
"calendar": "Calendari",
"about": "Sobre aquesta agenda"
"about": "Sobre aquesta agenda",
"content": "Contingut",
"admin_actions": "Accions d'administració",
"recurring_event_actions": "Accions d'activitats recorrents",
"tag": "Etiqueta"
},
"login": {
"description": "Amb la sessió iniciada pots afegir activitats noves.",
@ -175,7 +179,7 @@
"saved": "S'ha desat l'activitat",
"import_description": "Pots importar activitats des d'altres instàncies o plataformes que facin servir formats estàndards (ics o h-event)",
"remove_media_confirmation": "Confirmeu l'eliminació de la imatge?",
"download_flyer": "Baixa el flyer",
"download_flyer": "Baixa el cartell",
"alt_text_description": "Descripció per a persones amb discapacitat visual",
"choose_focal_point": "Tria el punt focal",
"address_description": "Quina és l'adreça completa del lloc?",
@ -270,7 +274,35 @@
"fallback_image": "Cartell per defecte",
"config_plugin": "Configura el complement",
"header_image": "Imatge de capçalera",
"default_images": "Cartell per defecte"
"default_images": "Cartell per defecte",
"blocked": "Bloquejat/da",
"domain": "Domini",
"known_users": "Usuàries conegudes",
"created_at": "Creada",
"default_images_help": "<a href='/admin?tab=theme'>Actualitza la pàgina</a> per veure els canvis.",
"geocoding_countrycodes": "Codis d'estats",
"admin_email_help": "L'adreça que es posa de remitent per enviar correus. També és l'adreça a la qual s'envien els correus d'administració",
"delete_collection_confirm": "Segur que vols eliminar la coŀlecció <u>{collection}</u>?",
"geocoding_provider_type_help": "Per defecte es resolen les adreces amb Nominatim",
"geocoding_provider": "Proveïdora de geocodificació",
"geocoding_provider_type": "Programari de geocodificació",
"geocoding_provider_help": "Per defecte es geocodifiquen les adreces amb Nominatim",
"geocoding_countrycodes_help": "Permet aplicar un filtre per cercar només dins de les fronteres de l'estat",
"geocoding_test_button": "Prova la geocodificació",
"geocoding_test_success": "La geocodificació de {service_name} funciona",
"geocoding_test_error": "El servei de geocodificació de {service_name} no està funcionant",
"tilelayer_provider": "Proveïdora de tesseŀles del mapa base",
"tilelayer_provider_help": "Per defecte les tesseŀles del mapa es baixen del servidor central d'OpenStreetMap",
"tilelayer_test_button": "Prova el mapa base",
"tilelayer_test_success": "El servei de tesseŀles de {service_name} està funcionant",
"tilelayer_test_error": "El servei de tesseŀles de {service_name} no està funcionant",
"geolocation": "Geolocalització",
"edit_tag_help": "Pots canviar l'etiqueta reanomenant-la o bé fusionant-la amb una d'existent. S'actualitzaran les {n} activitats que la fan servir.",
"allow_multidate_event": "Permet activitats de diversos dies",
"delete_tag_confirm": "Segur que vols eliminar l'etiqueta \"{tag}\"? S'esborrarà del sistema i de {n} activitats.",
"edit_tag": "Canvia l'etiqueta",
"tilelayer_provider_attribution": "Atribució",
"geolocation_description": "<b>1. Defineix una proveïdora de geocodificació</b>.<br>Actualment, de totes les llistades en <a href=\"https://wiki.openstreetmap.org/wiki/Nominatim#Alternatives_.2F_Third-party_providers\">la wiki d'OpenStreetMap</a>, són compatibles amb Gancio <a href=\"https://github.com/osm-search/Nominatim\">Nominatim</a> i <a href=\"https://github.com/komoot/photon\">Photon</a>.<br>Pots fer servir les instaŀlacions d'aquestes proveïdores copiant l'adreça de la instaŀlació al camp de 'Proveïdora de geolocodificació'<ul><li>https://nominatim.openstreetmap.org/search (<a href=\"https://operations.osmfoundation.org/policies/nominatim/\">Condicions del servei</a>)</li><li>https://photon.komoot.io/api/ (<a href=\"https://photon.komoot.io/\">Condicions del servei</a>)</li></ul><br><b>2. Defineix una proveïdora per mapes base.</b><br>Pots trobar-ne una llista aquí: <a href=\"https://leaflet-extras.github.io/leaflet-providers/preview/\">https://leaflet-extras.github.io/leaflet-providers/preview/</a>"
},
"auth": {
"not_confirmed": "Encara no s'ha confirmat…",

View file

@ -97,7 +97,10 @@
"home": "Startseite",
"about": "Über",
"plugins": "Plugins",
"help_translate": "Hilf beim Übersetzen mit"
"help_translate": "Hilf beim Übersetzen mit",
"content": "Inhalt",
"admin_actions": "Aktionen der Administrierenden",
"recurring_event_actions": "Einstellungen für regelmäßige Veranstaltungen"
},
"admin": {
"delete_footer_link_confirm": "Möchtest du diesen Link löschen?",
@ -129,7 +132,7 @@
"wrong_domain_warning": "Die \"baseurl\" die in config.json konfiguriert ist <b>({baseurl})</b> unterscheidet sich von derjenigen <b>({url})</b> die du besuchst",
"instance_place_help": "Diese Textzeile wird im Menü der anderen befreundeten Instanzen angezeigt",
"place_description": "Falls ein Ort falsch ist oder sich die Adresse ändert, kannst du ihn ändern.<br/>Bitte beachte, dass alle Veranstaltungen, die mit diesem Ort verbunden sind, die Adresse ändern (auch zurückliegende).",
"enable_admin_user_confirm": "Achte darauf, dass du der nutzenden Person {user} Admin-Rechte hinzufügst?",
"enable_admin_user_confirm": "Bist du dir sicher, dass du der nutzenden Person {user} Admin-Rechte gewährst?",
"trusted_instances_help": "Befreundete Instanzen werden in der Navigationsleiste oben auf der Seite angezeigt",
"trusted_instances_label": "Navigationsbezeichnung zu Friend-Instanzen",
"trusted_instances_label_default": "Freundliche Instanzen",
@ -184,7 +187,32 @@
"default_images": "Standard Bilder",
"config_plugin": "Plugin Konfiguration",
"hide_thumbs": "Vorschaubilder ausblenden",
"hide_calendar": "Kalender verstecken"
"hide_calendar": "Kalender verstecken",
"admin_email_help": "Die Adresse, die wir als Absender für den Versand von E-Mails verwenden. Sie ist auch die Adresse, an die deine E-Mails an die Administrator:innen geschickt werden.",
"blocked": "Geblockt",
"domain": "Domain",
"known_users": "Bekannte Nutzer:innen",
"created_at": "Erstellt am",
"geocoding_provider_type": "Software für Georeferenzierung",
"geocoding_provider_type_help": "Die Standard-Software ist Nominatim",
"geocoding_provider": "Anbieter von Geocodierung",
"geocoding_provider_help": "Der Standard-Anbieter ist Nominatim",
"geocoding_countrycodes": "Gebietskennziffern",
"geocoding_countrycodes_help": "Ermöglicht die Einrichtung eines Filters für die Suche auf der Grundlage von Ländercodes",
"geocoding_test_button": "Geokodierung testen",
"geocoding_test_success": "Der Geokodierdienst unter {service_name} funktioniert",
"geocoding_test_error": "Der Dienst ist unter der angegebenen Adresse nicht zu erreichen: {service_name}",
"tilelayer_provider": "Kachel-LayerAnbieter",
"tilelayer_provider_help": "Der Standard-Anbieter ist OpenStreetMap",
"tilelayer_provider_attribution": "Namensnennung",
"tilelayer_test_button": "Kachel-Layer testen",
"tilelayer_test_success": "Der Kachel-Layer-Dienst unter {service_name} funktioniert",
"tilelayer_test_error": "Der Dienst ist unter der angegebenen Adresse nicht zu erreichen: {service_name}",
"geolocation": "Geolokation",
"allow_multidate_event": "Lasse mehrtägige Veranstaltungen zu",
"admin_email": "E-Mail von der administrierenden Person",
"geolocation_description": "<b>1. Bestimme einen Anbieter für einen Geokodierdienst</b>.<br>Derzeit gibt es unter den im <a href=\"https://wiki.openstreetmap.org/wiki/Nominatim#Alternatives_.2F_Third-party_providers\">Wiki von OpenStreetMap</a>, Anbietern Unterstützung für die Software <a href=\"https://github.com/osm-search/Nominatim\">Nominatim</a> und <a href=\"https://github.com/komoot/photon\">Photon</a>.<br>Du kannst eine der entsprechenden offiziellen Demos verwenden, indem du den Link in das Feld \"Geocoding provider\" kopierst:<ul><li>https://nominatim.openstreetmap.org/search (<a href=\"https://operations.osmfoundation.org/policies/nominatim/\">Terms of Service</a>)</li><li>https://photon.komoot.io/api/ (<a href=\"https://photon.komoot.io/\">Terms of Service</a>)</li></ul><br><b>2. Definiere einen Anbieter für Kartenebenen.</b><br>Eine Liste von ihnen findest du hier: <a href=\"https://leaflet-extras.github.io/leaflet-providers/preview/\">https://leaflet-extras.github.io/leaflet-providers/preview/</a>",
"default_images_help": "Du musst <a href='/admin?tab=theme'>die Seite neu laden</a>, um die Änderungen sehen zu können."
},
"settings": {
"update_confirm": "Willst du deine Änderung speichern?",

29
locales/email/ru.json Normal file
View file

@ -0,0 +1,29 @@
{
"recover": {
"subject": "Восстановление пароля",
"content": "Здравствуйте, вы запросили восстановление пароля на {{config.title}}. <a href='{{config.baseurl}}/recover/{{user.recover_code}}'>Нажмите здесь</a> для подтверждения."
},
"confirm": {
"subject": "Теперь вы можете начать публиковать события",
"content": "Здравствуйте, ваша учетная запись <a href='{{config.baseurl}}'>{{config.title}}</a> была подтверждена. Пишите нам по адресу {{config.admin_email}} для получения любой информации."
},
"admin_register": {
"subject": "Новая регистрация",
"content": "{{user.email}} запросил регистрацию на {{config.title}}: <br/><pre>{{user.description}}</pre><br/> Подтвердите это <a href='{{config.baseurl}}/admin'>здесь</a>."
},
"user_confirm": {
"subject": "Теперь вы можете начать публиковать события",
"content": "Здравствуйте, ваша учетная запись <a href='{{config.baseurl}}'>{{config.title}}</a> была создана. <a href='{{config.baseurl}}/user_confirm/{{user.recover_code}}'>Подтвердите это и измените пароль.</a>."
},
"register": {
"content": "Мы получили запрос на регистрацию. Мы подтвердим его как можно скорее.",
"subject": "Запрос на регистрацию получен"
},
"event_confirm": {
"content": "Вы можете подтвердить это событие по адресу <a href='{{url}}'>на этой странице</a>"
},
"test": {
"subject": "Ваша конфигурация SMTP работает",
"content": "Это тестовое письмо, если вы читаете его, ваша конфигурация работает."
}
}

View file

@ -1 +0,0 @@
{}

View file

@ -82,6 +82,7 @@
"url": "URL",
"place": "Place",
"tags": "Tags",
"tag": "Tag",
"theme": "Theme",
"reset": "Reset",
"import": "Import",
@ -146,6 +147,7 @@
"recurrent": "Recurring",
"edit_recurrent": "Edit recurring event:",
"show_recurrent": "recurring events",
"show_multidate": "multidate events",
"show_past": "also prior events",
"only_future": "only upcoming events",
"recurrent_description": "Choose frequency and select days",
@ -192,6 +194,7 @@
"event_remove_ok": "Event removed",
"allow_registration_description": "Allow open registrations?",
"allow_anon_event": "Allow anonymous events (has to be confirmed)?",
"allow_multidate_event": "Allow multi-day events",
"allow_recurrent_event": "Allow recurring events",
"allow_geolocation": "Allow events geolocation",
"recurrent_event_visible": "Show recurring events by default",
@ -212,6 +215,7 @@
"hide_resource": "Hide resource",
"delete_resource": "Delete resource",
"delete_resource_confirm": "Are you sure you want to delete this resource?",
"delete_tag_confirm": "Are you sure you want to remove the tag \"{tag}\"? The tag will be removed from {n} events.",
"block_user": "Block user",
"filter_instances": "Filter instances",
"filter_users": "Filter users",
@ -243,6 +247,8 @@
"footer_links": "Footer links",
"delete_footer_link_confirm": "Sure to remove this link?",
"edit_place": "Edit place",
"edit_tag": "Edit tag",
"edit_tag_help": "You can change the tag by replacing it with a new one or merging it with an existing one. The {n} associated events will also be changed.",
"new_announcement": "New announcement",
"show_smtp_setup": "Email settings",
"smtp_hostname": "SMTP Hostname",
@ -252,12 +258,14 @@
"smtp_test_success": "A test email is sent to {admin_email}, please check your inbox",
"smtp_test_button": "Send a test email",
"smtp_use_sendmail": "Use sendmail",
"sender_email": "Sender e-mail",
"admin_email": "Admin e-mail",
"admin_email_help": "The address we use as the sender to send emails. This is also the address to which admin emails are sent",
"widget": "Widget",
"wrong_domain_warning": "The baseurl configured in config.json <b>({baseurl})</b> differs from the one you're visiting <b>({url})</b>",
"new_collection": "New collection",
"collections_description": "Collections are groupings of events by tags and places. They will be displayed on the home page",
"edit_collection": "Edit Collection",
"delete_collection_confirm": "Are you sure you want to remove the collection <u>{collection}</u>?",
"config_plugin": "Plugin configuration",
"plugins_description": "",
"fallback_image": "Fallback image",
@ -265,10 +273,28 @@
"hide_thumbs": "Hide thumbs",
"hide_calendar": "Hide calendar",
"default_images": "Default images",
"default_images_help": "You have to <a href='/admin?tab=theme'>refresh</a> the page to see the changes.",
"blocked": "Blocked",
"domain": "Domain",
"known_users": "Known users",
"created_at": "Created at"
"created_at": "Created at",
"geolocation_description": "<b>1. Define a provider for geocoding service</b>.<br>Currently, among those listed in the <a href=\"https://wiki.openstreetmap.org/wiki/Nominatim#Alternatives_.2F_Third-party_providers\">wiki of OpenStreetMap</a>, there is support for software <a href=\"https://github.com/osm-search/Nominatim\">Nominatim</a> and <a href=\"https://github.com/komoot/photon\">Photon</a>.<br>You can use one of the related official demos by copying the link in the 'Geocoding provider' field:<ul><li>https://nominatim.openstreetmap.org/search (<a href=\"https://operations.osmfoundation.org/policies/nominatim/\">Terms of Service</a>)</li><li>https://photon.komoot.io/api/ (<a href=\"https://photon.komoot.io/\">Terms of Service</a>)</li></ul><br><b>2. Define a provider for map layers.</b><br>You can find a list of them here: <a href=\"https://leaflet-extras.github.io/leaflet-providers/preview/\">https://leaflet-extras.github.io/leaflet-providers/preview/</a>",
"geocoding_provider_type": "Geocoding software",
"geocoding_provider_type_help": "The default software is Nominatim",
"geocoding_provider": "Geocoding provider",
"geocoding_provider_help": "The default provider is Nominatim",
"geocoding_countrycodes": "Country codes",
"geocoding_countrycodes_help": "Allows you to set a filter to searches based on area codes",
"geocoding_test_button": "Test geocoding",
"geocoding_test_success": "The geocoding service at {service_name} is working",
"geocoding_test_error": "The geocoding service is not reachable at {service_name}",
"tilelayer_provider": "Tilelayer provider",
"tilelayer_provider_help": "The default provider is OpenStreetMap",
"tilelayer_provider_attribution": "Attribution",
"tilelayer_test_button": "Test tilelayer",
"tilelayer_test_success": "The tilelayer service at {service_name} is working",
"tilelayer_test_error": "The tilelayer service is not reachable at {service_name}",
"geolocation": "Geolocation"
},
"auth": {
"not_confirmed": "Not confirmed yet…",

View file

@ -64,7 +64,7 @@
"embed": "Incorporar",
"embed_title": "Publica este evento en tu página web",
"embed_help": "Copiando el siguiente código en tu página web, el evento será incluido como puedes ver aquí al lado",
"feed": "RSS Feed",
"feed": "Canal RSS",
"feed_url_copied": "Copiada feed url, pegala en tu lector de feeds",
"follow_me_title": "Sigue las actualizaciones en el fediverso",
"follow": "Sigue",
@ -98,7 +98,11 @@
"about": "Sobre el sitio",
"close": "Cerrar",
"help_translate": "Ayuda a traducir",
"calendar": "Calendario"
"calendar": "Calendario",
"content": "Contenido",
"admin_actions": "Acciones de administrador",
"recurring_event_actions": "Acciones de eventos recurrentes",
"tag": "Etiqueta"
},
"login": {
"description": "Entrando podrás publicar nuevos eventos.",
@ -179,7 +183,8 @@
"address_description": "¿Cuál es la dirección?",
"alt_text_description": "Descripción para personas con baja visión",
"download_flyer": "Descargar folleto",
"remove_media_confirmation": "¿Confirmas la eliminación de la imagen?"
"remove_media_confirmation": "¿Confirmas la eliminación de la imagen?",
"show_multidate": "eventos de varios días"
},
"admin": {
"place_description": "En el caso de que un lugar sea incorrecto o cambie de dirección, puedes cambiarlo. <br/> Todos los eventos presentes y pasados asociados con este lugar cambiarán de dirección.",
@ -268,7 +273,35 @@
"smtp_use_sendmail": "Usar sendmail",
"wrong_domain_warning": "El parámetro baseurl configurado en config.json <b>({baseurl})</b> difiere del que estás visitando <b>({url})</b>",
"new_collection": "Nueva colección",
"collections_description": "Las colecciones son agrupaciones de eventos por etiquetas y ubicaciones. Serán desplegadas en la página de inicio"
"collections_description": "Las colecciones son agrupaciones de eventos por etiquetas y ubicaciones. Serán desplegadas en la página de inicio",
"domain": "Dominio",
"created_at": "Creado a las",
"blocked": "Bloqueado",
"known_users": "Usuarios conocidos",
"default_images_help": "Tienes que <a href='/admin?tab=theme'>actualizas</a> la página para ver los cambios.",
"geocoding_provider": "Proveedor de geocodificación",
"admin_email_help": "La dirección que usamos como remitente para enviar emails. Esta es también la dirección a la cual los emails de administración son enviados",
"allow_multidate_event": "Permitir eventos de múltiples días",
"geocoding_countrycodes": "Códigos de país",
"geocoding_provider_help": "El proveedor por defecto es Nominatim",
"geocoding_provider_type": "Software de geocodificación",
"geocoding_provider_type_help": "El programa por defecto es Nominatim",
"geocoding_countrycodes_help": "Permite establecer un filtro para las búsquedas basadas en códigos de área",
"geocoding_test_button": "Prueba de geocodificación",
"delete_collection_confirm": "¿Estás seguro de que quieres eliminar la colección <u>{collection}</u>?",
"geocoding_test_success": "El servicio de geocodificación de {service_name} funciona",
"tilelayer_provider": "Proveedor de mosaicos de mapas",
"tilelayer_provider_help": "El proveedor por defecto es OpenStreetMap",
"tilelayer_provider_attribution": "Atribución",
"tilelayer_test_button": "Capa de mosaicos de prueba",
"tilelayer_test_success": "El servicio de mosaicos en {service_name} está funcionando",
"tilelayer_test_error": "No se puede acceder al servicio de mosaico en {service_name}",
"geolocation": "Geolocalización",
"delete_tag_confirm": "¿Está seguro de que desea eliminar la etiqueta \"{tag}\"? La etiqueta se eliminará de {n} eventos.",
"edit_tag": "Editar la etiqueta",
"edit_tag_help": "Puede cambiar la etiqueta sustituyéndola por una nueva o fusionándola con una existente. Los {n} eventos asociados también se modificarán.",
"geolocation_description": "<b>1. Defina un proveedor para el servicio de codificación geográfica</b>.<br>Actualmente, entre los enumerados en el <a href=\"https://wiki.openstreetmap.org/wiki/Nominatim#Alternatives_.2F_Third-party_providers\">wiki de OpenStreetMap </a>, hay soporte para el software <a href=\"https://github.com/osm-search/Nominatim\">Nominatim</a> y <a href=\"https://github.com/komoot /photon\">Photon</a>.<br>Puede usar una de las demostraciones oficiales relacionadas copiando el enlace en el campo 'Proveedor de codificación geográfica':<ul><li>https://nominatim.openstreetmap.org/search (<a href=\"https://operations.osmfoundation.org/policies/nominatim/\">Términos de servicio</a>)</li><li>https://photon.komoot.io/api/ (<a href=\"https://photon.komoot.io/\"> Condiciones de servicio</a>)</li></ul><br><b>2. Defina un proveedor para las capas del mapa.</b><br>Puede encontrar una lista de ellos aquí: <a href=\"https://leaflet-extras.github.io/leaflet-providers/preview/\">https://leaflet-extras.github.io/leaflet-providers/preview/</a>",
"geocoding_test_error": "No se puede acceder al servicio de geocodificación en {service_name}"
},
"auth": {
"not_confirmed": "Todavía no hemos confirmado este email…",

View file

@ -25,7 +25,7 @@
"events": "Ekitaldiak",
"places": "Lekuak",
"settings": "Aukerak",
"actions": "Ekintzak",
"actions": "Eragiketak",
"deactivate": "Desaktibatu",
"remove_admin": "Kendu administratzaile baimena",
"activate": "Aktibatu",
@ -95,7 +95,10 @@
"show_map": "Erakutsi mapa",
"calendar": "Egutegia",
"home": "Etxea",
"about": "Honi buruz"
"about": "Honi buruz",
"recurring_event_actions": "Ekitaldi errepikarien eragiketak",
"content": "Edukia",
"admin_actions": "Administratzaile eragiketak"
},
"login": {
"description": "Saioa hasten baduzu ekitaldi berriak sortu ahal izango dituzu.",
@ -265,7 +268,30 @@
"config_plugin": "Pluginaren konfigurazioa",
"fallback_image": "Lehenetsitako irudia",
"header_image": "Goiburuko irudia",
"default_images": "Lehenetsitako irudiak"
"default_images": "Lehenetsitako irudiak",
"blocked": "Blokeatuta",
"domain": "Domeinua",
"default_images_help": "Orrialdea <a href='/admin?tab=theme'>freskatu</a> behar duzu aldaketak ikusteko.",
"known_users": "Erabiltzaile ezagunak",
"created_at": "Noiz sortua:",
"geocoding_provider_type_help": "Software lehenetsia Nominatim da",
"geocoding_provider": "Geokodeketaren hornitzailea",
"geocoding_countrycodes": "Estatuen kodeak",
"geocoding_countrycodes_help": "Baimendu iragazki bat ezartzen zonalde kodearen arabera",
"geocoding_test_button": "Probatu geokodeketa",
"tilelayer_provider_help": "Lehenetsitako hornitzailea OpenStreetMap da",
"tilelayer_provider_attribution": "Atribuzioa",
"geocoding_test_error": "Geokodeketa zerbitzua {service_name}-(e)n ez dago atzigarri",
"tilelayer_provider": "Lauza-geruzen hornitzailea",
"tilelayer_test_button": "Probatu lauza-geruza",
"tilelayer_test_error": "Lauza-geruzen zerbitzua {service_name}(e)n ez dago atzigarri",
"admin_email_help": "Epostak bidaltzeko erabiltzen dugun helbidea. Administrazio epostak ere horra bidaltzen dira",
"allow_multidate_event": "Baimendu egun anitzeko ekitaldiak",
"geocoding_provider_type": "Geokodeketarako softwarea",
"geocoding_provider_help": "Lehenetsitako hornitzailea Nominatim da",
"geolocation": "Geokokapena",
"geocoding_test_success": "Geokodeketa zerbitzua {service_name}-(e)n martxan dago",
"tilelayer_test_success": "Lauza-geruzen zerbitzua {service_name}(e)n martxan dago"
},
"auth": {
"not_confirmed": "Oraindik baieztatu gabe dago…",

View file

@ -93,7 +93,16 @@
"show_map": "Mostrar mapa",
"latitude": "Latitude",
"longitude": "Longitude",
"getting_there": "Chegar lá"
"getting_there": "Chegar lá",
"plugins": "Complementos",
"help_translate": "Axuda coa tradución",
"calendar": "Calendario",
"home": "Inicio",
"about": "Acerca de",
"content": "Contido",
"admin_actions": "Accións de Admin",
"recurring_event_actions": "Accións de eventos recurrentes",
"tag": "Etiqueta"
},
"recover": {
"not_valid_code": "Algo fallou."
@ -134,7 +143,7 @@
"interact_with_me": "Sígueme",
"remove_recurrent_confirmation": "Tes a certeza de querer eliminar este evento recurrente?\nOs eventos pasados permanecerán, pero non se crearán novos eventos.",
"import_URL": "Importar desde URL",
"anon": "Anon",
"anon": "Anónimo",
"anon_description": "Podes engadir un evento sen rexistrarte nen acceder, pero deberás agardar a que alguén o lea e\nconfirme que é un evento axeitado. Non será posible modificalo.<br/><br/>\nPodes tamén <a href='/login'>acceder</a> ou <a href='/register'>crear unha conta</a>. Mais podes continuar e agardar a que se comprobe. ",
"same_day": "no mesmo día",
"tag_description": "Cancelo",
@ -163,7 +172,10 @@
"alt_text_description": "Descrición para persoas con problemas de visión",
"choose_focal_point": "Elixe onde centrar a atención",
"remove_media_confirmation": "Confirmas a eliminación da imaxe?",
"download_flyer": "Descargar folleto"
"download_flyer": "Descargar folleto",
"address_description": "Cal é o enderezo?",
"address_description_osm": "Cal é o enderezo? (Contribuíntes a <a href='http://osm.org/copyright'>OpenStreetMap</a>)",
"show_multidate": "eventos con varias datas"
},
"admin": {
"place_description": "Se escribiches mal o lugar ou enderezo, podes cambialo.<br/>Cambiará o enderezo de tódolos eventos actuais e pasados asociados a este lugar.",
@ -247,8 +259,42 @@
"disable_admin_user_confirm": "Tes certeza de querer retirar os permisos de administración a {user}?",
"sender_email": "Remitente",
"collections_description": "As coleccións son agrupamentos de eventos por etiqueta ou lugar. Pode ser mostrado na páxina de inicio",
"enable_admin_user_confirm": "Tes a certeza de querer darlle permisos de administración a {user}?",
"smtp_use_sendmail": "Usar sendmail"
"enable_admin_user_confirm": "Tes a certeza de engadir permiso de admin a {user}?",
"smtp_use_sendmail": "Usar sendmail",
"config_plugin": "Configuración do complemento",
"known_users": "Usuarias coñecidas",
"created_at": "Creado o",
"fallback_image": "Imaxe por omisión",
"header_image": "Imaxe da cabeceira",
"hide_thumbs": "Agochar miniaturas",
"hide_calendar": "Agochar calendario",
"default_images": "Imaxes por defecto",
"blocked": "Bloqueado",
"domain": "Dominio",
"default_images_help": "Tes que <a href='/admin?tab=theme'>actualizar</a> a páxina para ver os cambios.",
"geocoding_provider_type": "Software Geocoding",
"geocoding_provider_type_help": "O software por defecto é Nominatim",
"geocoding_provider": "Provedor Geocoding",
"geocoding_provider_help": "O provedor por defecto é Nominatim",
"geocoding_countrycodes": "Códigos de país",
"geocoding_countrycodes_help": "Permíteche establecer un filtro para as buscas en función do código",
"geocoding_test_button": "Comproba a codificación",
"geocoding_test_success": "O servizo geocoding en {service_name} funciona",
"geocoding_test_error": "O servizo geocoding non está accesible en {service_name}",
"tilelayer_provider": "Provedor de teselas do mapa",
"tilelayer_provider_help": "O provedor por defecto é OpenStreetMap",
"tilelayer_provider_attribution": "Atribución",
"tilelayer_test_button": "Comproba as capas",
"tilelayer_test_success": "O servizo de capas en {service_name} funciona",
"tilelayer_test_error": "O servizo de capas en {service_name} non está accesible",
"geolocation": "Xeolocalización",
"allow_multidate_event": "Permitir eventos de varios días",
"admin_email_help": "O enderezo que se usará como remitente para os emails. Tamén é o enderezo ao que se envían emails de administración",
"geolocation_description": "<b>1. Define un provedor para o servizo geocoding</b>.<br>Actualmente, entre os que aparecen na <a href=\"https://wiki.openstreetmap.org/wiki/Nominatim#Alternatives_.2F_Third-party_providers\">wiki de OpenStreetMap</a>, temos soporte para <a href=\"https://github.com/osm-search/Nominatim\">Nominatim</a> e <a href=\"https://github.com/komoot/photon\">Photon</a>.<br>Podes usar unha das demos oficiais indicadas copiando a ligazón no campo 'Provedor Geocoding':<ul><li>https://nominatim.openstreetmap.org/search (<a href=\"https://operations.osmfoundation.org/policies/nominatim/\">Termos do Servizo</a>)</li><li>https://photon.komoot.io/api/ (<a href=\"https://photon.komoot.io/\">Termos do Servizo</a>)</li></ul><br><b>2. Define un provedor para capas do mapa.</b><br>Aquí hai unha lista: <a href=\"https://leaflet-extras.github.io/leaflet-providers/preview/\">https://leaflet-extras.github.io/leaflet-providers/preview/</a>",
"edit_tag": "Editar etiqueta",
"edit_tag_help": "Podes cambiar a etiqueta substituíndoa por unha nova ou fusionándoa cunha existente. Tamén se cambiarán os {n} eventos asociados.",
"delete_tag_confirm": "Estás seguro de que queres eliminar a etiqueta \"{tag}\"? A etiqueta eliminarase de {n} eventos.",
"delete_collection_confirm": "Estás seguro de que queres eliminar a colección <u>{collection}</u>?"
},
"auth": {
"not_confirmed": "Aínda non foi confirmado…",

View file

@ -10,6 +10,7 @@ module.exports = {
nb: 'Norwegian Bokmål',
pl: 'Polski',
pt: 'Português',
ru: 'Русский',
sk: 'Slovak',
zh: '中国'
}

View file

@ -136,9 +136,9 @@
"saved": "Evento salvato",
"added_anon": "Evento aggiunto, verrà confermato quanto prima.",
"updated": "Evento aggiornato",
"where_description": "Dov'è il gancio? Se il posto non è presente potrai crearlo.",
"where_description": "Dov'è l'evento? Se il posto non è presente potrai crearlo.",
"address_description": "A che indirizzo?",
"address_description_osm": "A che indirizzo? ((<a href='http://osm.org/copyright'>OpenStreetMap</a>)",
"address_description_osm": "A che indirizzo? (<a href='http://osm.org/copyright'>OpenStreetMap</a>)",
"coordinates_search_description": "Puoi ricercare il posto per nome, o incollare la coppia di coordinate.",
"confirmed": "Evento confermato",
"not_found": "Evento non trovato",
@ -147,6 +147,7 @@
"recurrent": "Ricorrente",
"edit_recurrent": "Modifica evento ricorrente:",
"show_recurrent": "appuntamenti ricorrenti",
"show_multidate": "eventi di più giorni",
"show_past": "eventi passati",
"recurrent_description": "Scegli la frequenza e seleziona i giorni",
"multidate_description": "Un festival o una tre giorni? Scegli quando comincia e quando finisce",
@ -240,6 +241,8 @@
"footer_links": "Collegamenti del piè di pagina",
"delete_footer_link_confirm": "Vuoi eliminare questo collegamento?",
"edit_place": "Modifica luogo",
"edit_tag": "Modifica tag",
"edit_tag_help": "Puoi cambiare il tag sostituendolo con uno nuovo o unendolo ad uno gia' esistente. Verranno modificati anche i {n} eventi associati",
"new_announcement": "Nuovo annuncio",
"show_smtp_setup": "Impostazioni email",
"widget": "Widget",
@ -248,6 +251,7 @@
"smtp_test_success": "Una mail di test è stata inviata all'indirizzo {admin_email}, controlla la tua casella di posta",
"smtp_test_button": "Invia una mail di prova",
"admin_email": "E-mail dell'admin",
"admin_email_help": "L'indirizzo che usiamo come mittente per inviare le e-mail. È anche l'indirizzo a cui vengono spedite le e-mail di amministrazione",
"new_collection": "Crea bolla",
"wrong_domain_warning": "Il \"baseurl\" configurato in config.json <b>({baseurl})</b> è diverso da quello che stai visitando <b>({url})</b>",
"collections_description": "Le bolle sono raggruppamenti di eventi per tag e posti.",
@ -257,7 +261,29 @@
"header_image": "Immagine di intestazione",
"hide_thumbs": "Nascondi immaginine",
"hide_calendar": "Nascondi calendario",
"default_images": "Immagini preimpostate"
"default_images": "Immagini preimpostate",
"default_images_help": "Devi <a href='/admin?tab=theme'>aggiornare</a> la pagina per vedere le modifiche.",
"blocked": "Bloccato",
"domain": "Domini",
"known_users": "Utenti conosciuti",
"created_at": "Creato il",
"geolocation_description": "<b>1. Definisci un fornitore per il servizio di georeferenziazione (geocodifica)</b>.<br>Al momento, tra quelli elencati nella <a href=\"https://wiki.openstreetmap.org/wiki/Nominatim#Alternatives_.2F_Third-party_providers\">wiki di OpenStreetMap</a>, è presente il supporto per i software <a href=\"https://github.com/osm-search/Nominatim\">Nominatim</a> e <a href=\"https://github.com/komoot/photon\">Photon</a>.<br>Puoi utilizzare una delle relative demo ufficiali copiandone il link nel campo 'Fornitore georeferenziazione':<ul><li>https://nominatim.openstreetmap.org/search (<a href=\"https://operations.osmfoundation.org/policies/nominatim/\">Terms of Service</a>)</li><li>https://photon.komoot.io/api/ (<a href=\"https://photon.komoot.io/\">Terms of Service</a>)</li></ul><br><b>2. Definisci un fornitore di layers per la mappa.</b><br>Qui puoi trovarne una lista: <a href=\"https://leaflet-extras.github.io/leaflet-providers/preview/\">https://leaflet-extras.github.io/leaflet-providers/preview/</a>",
"geocoding_provider_type": "Software fornitore georeferenziazione",
"geocoding_provider_type_help": "Il software di default è Nominatim",
"geocoding_provider": "Fornitore georeferenziazione",
"geocoding_provider_help": "Il fornitore di default è Nominatim",
"geocoding_countrycodes": "Codici territoriali",
"geocoding_countrycodes_help": "Permette di impostare un filtro alle ricerche basato su codici territori nazionali",
"geocoding_test_button": "Test geocoding",
"geocoding_test_success": "Il servizio di geocoding all'indirizzo {service_name} sta funzionando",
"geocoding_test_error": "Il servizio non è raggiungibile all'indirizzo: {service_name}",
"tilelayer_provider": "Fornitore tilelayer",
"tilelayer_provider_help": "Il fornitore di default è OpenStreetMap",
"tilelayer_provider_attribution": "Attribuzione",
"tilelayer_test_button": "Test tilelayer",
"tilelayer_test_success": "Il servizio di tilelayer all'indirizzo {service_name} sta funzionando",
"tilelayer_test_error": "Il servizio non è raggiungibile all'indirizzo: {service_name}",
"geolocation": "Geo e mappe"
},
"auth": {
"not_confirmed": "Non ancora confermato…",

View file

@ -1,6 +1,6 @@
{
"common": {
"add_event": "Adicionar Evento",
"add_event": "Adicionar evento",
"description": "Descrição",
"send": "Enviar",
"address": "Endereço",
@ -13,7 +13,7 @@
"what": "O que",
"media": "Media",
"password": "Senha",
"register": "Registro",
"register": "Registrar",
"remove": "Remover",
"confirm": "Confirmar",
"events": "Eventos",
@ -22,8 +22,8 @@
"edit": "Editar",
"admin": "Admin",
"places": "Lugares",
"hide": "Esconder",
"search": "Procurar",
"hide": "Ocultar",
"search": "Buscar",
"info": "Info",
"users": "Usuários",
"share": "Compartilhar",
@ -76,11 +76,11 @@
"logout_ok": "Deslogado",
"n_resources": "nenhum recurso|um recurso|{n} recursos",
"embed": "Incorporar",
"embed_title": "Incorpore este evento na sua página",
"embed_help": "Copie o código seguinte em sua página e o evento será apresentado desta maneira",
"embed_title": "Incorpore este evento em sua página",
"embed_help": "Copie o seguinte código em sua página e o evento será exibido desta maneira",
"displayname": "Nome de exibição",
"feed_url_copied": "Abra a URL do feed no seu leitor RSS",
"follow_me_title": "Seguir atualizações a partir do Fediverso",
"feed_url_copied": "Abra a URL copiada do feed em seu leitor de RSS",
"follow_me_title": "Siga as atualizações pelo Fediverso",
"tags": "Marcadores",
"theme": "Tema",
"reset": "Reiniciar",
@ -88,18 +88,27 @@
"collections": "Coleções",
"max_events": "N. máximo de eventos",
"label": "Etiqueta",
"close": "Fechar"
"close": "Fechar",
"plugins": "Plugins",
"help_translate": "Ajude a traduzir",
"show_map": "Mostrar mapa",
"calendar": "Calendário",
"home": "Início",
"about": "Sobre",
"content": "Conteúdo",
"admin_actions": "Ações de admin",
"recurring_event_actions": "Ações de eventos recorrentes"
},
"admin": {
"user_block_confirm": "Você está certo que quer bloquer o usuário {user}?",
"user_block_confirm": "Você está certo que quer bloquear o usuário {user}?",
"filter_instances": "Filtrar instâncias",
"user_add_help": "Um e-mail con instruções para confirmar a inscrição e escolher uma senha será enviada ao novo usuário",
"user_add_help": "Um e-mail com instruções para confirmar a inscrição e escolher uma senha será enviada ao novo usuário",
"show_resource": "Mostrar recurso",
"block_user": "Bloquear usuário",
"filter_users": "Filtrar usuários",
"hide_resource": "Ocultar recurso",
"delete_resource": "Remover recurso",
"delete_resource_confirm": "Você está certo que quer remover esse recurso?",
"delete_resource_confirm": "Você está certo que quer remover este recurso?",
"show_smtp_setup": "Configurações de e-mail",
"resources": "Recursos",
"delete_announcement_confirm": "Você está certo que quer remover o anúncio?",
@ -107,7 +116,7 @@
"collections_description": "Coleções são agrupamentos de eventos por marcadores e locais. Eles serão exibidos na página principal",
"new_collection": "Nova coleção",
"disable_admin_user_confirm": "Você está certo que quer remover permissões de administração de {user}?",
"enable_admin_user_confirm": "Você está certo que quer adicionar permissões de administrador para {user}",
"enable_admin_user_confirm": "Você está certo que quer adicionar permissões de administrador para {user}?",
"event_remove_ok": "Evento removido",
"smtp_description": "<ul><li>Administrador deve receber um e-mail quando um evento anônimo for adicionado (se habilitado).</li><li>Administrador deve receber um e-mail de requisição de registro (se habilitado).</li><li>Usuário deve receber um e-mail de solicitação de registro.</li><li>Usuário deve receber um e-mail de confirmação de registro.</li><li>Usuário deve recever um e-mail de confirmação quando registrado diretamente por um administrador.</li><li>Usuários devem receber um e-mail para recuperar a senha quando eles esquecerem ela</li></ul>",
"allow_registration_description": "Permitir registro aberto de usuários?",
@ -138,7 +147,7 @@
"smtp_secure": "SMTP Seguro (TLS ou STARTTLS)",
"smtp_use_sendmail": "Utilizar sendmail",
"instance_place_help": "A etiqueta para exibir em outras instâncias",
"delete_trusted_instance_confirm": "Você quer realmente remover esse item do menu de instâncias amigas?",
"delete_trusted_instance_confirm": "Você quer realmente remover este item do menu de instâncias amigas?",
"sender_email": "E-mail do remetente",
"widget": "Widget",
"wrong_domain_warning": "A baseurl configurado em config.json <b>({baseurl})</b> é diferente da que você está visitando <b>({url})</b>",
@ -160,7 +169,7 @@
"allow_recurrent_event": "Permitir eventos recorrentes",
"recurrent_event_visible": "Exibir eventos recorrentes por padrão",
"user_blocked": "Usuário {user} bloqueado",
"announcement_description": "Nesta seção você pode inserir um anúncio para ser exibido na página principal",
"announcement_description": "Nesta seção você pode inserir anúncios que serão exibidos na página principal",
"description_description": "Exibido no cabeçalho próximo ao título",
"instance_place": "Local indicativo desta instância",
"is_dark": "Tema escuro",
@ -168,14 +177,29 @@
"new_announcement": "Novo anúncio",
"federation": "Federação / ActivityPub",
"smtp_test_success": "Um e-mail de teste foi enviado para {admin_email}, por favor verifique sua caixa de entrada",
"smtp_test_button": "Enviar e-mail de teste"
"smtp_test_button": "Enviar e-mail de teste",
"allow_geolocation": "Permitir geolocalização de eventos",
"config_plugin": "Configuração de plugin",
"fallback_image": "Imagem alternativa",
"header_image": "Imagem de cabeçalho",
"hide_thumbs": "Ocultar miniaturas",
"default_images_help": "Você precisa <a href='/admin?tab=theme'>recarregar</a> a página para ver as mudanças.",
"domain": "Domínio",
"default_images": "Imagens padrão",
"known_users": "Usuários conhecidos",
"created_at": "Criado em",
"hide_calendar": "Ocultar calendário",
"blocked": "Bloqueado",
"admin_email": "E-mail do admin",
"tilelayer_provider_attribution": "Atribuição",
"geolocation": "Geolocalização"
},
"event": {
"follow_me_description": "Uma das maneiras de se manter atualizado com os eventos publicados aqui em {title},\né seguir a conta <u>{account}</u> no Fediverso, por exemplo via Mastodon, e possivelmente adicionar recursos para um evento a partir de lá.<br/><br/>\nSe você nunca ouviu falar sobre Mastodon ou do Fediverso nós recomendamos ler <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>este artigo</a>.<br/><br/>Entre com sua instância abaixo (e.g. mastodon.social)",
"saved": "Evento salvo",
"recurrent": "Recorrente",
"ics": "ICS",
"recurrent_1m_days": "|O {days} de cada mês|{days} de cada mês",
"recurrent_1m_days": "Dia {days} de cada mês",
"interact_with_me": "Siga-me",
"media_description": "Você pode adicionar um flyer (opcional)",
"same_day": "no mesmo dia",
@ -202,12 +226,12 @@
"normal_description": "Escolha o dia.",
"recurrent_1w_days": "A cada {days}",
"each_week": "Toda semana",
"each_2w": "Todas outras semanas",
"each_month": "Cada mês",
"each_2w": "A cada duas semanas",
"each_month": "Todo mês",
"recurrent_2w_days": "{days} a cada dois",
"recurrent_2m_days": "|Dia {days} a cada dois meses|Os dias {days} a cada dois meses",
"recurrent_1m_ordinal": "O {n} {days} de cada mês",
"recurrent_2m_ordinal": "|O {n} {days} a cada dois meses|O {n} {days} a cada dois meses",
"recurrent_2m_days": "Dia {days} a cada dois meses",
"recurrent_1m_ordinal": "{n} {days} de cada mês",
"recurrent_2m_ordinal": "{n} {days} a cada dois meses",
"due": "até",
"from": "De",
"image_too_big": "A imagem não pode ser maior que 4MB",
@ -219,7 +243,9 @@
"alt_text_description": "Descrição para pessoas com deficiências visuais",
"choose_focal_point": "Escolha o ponto focal",
"remove_media_confirmation": "Você confirma a remoção da imagem?",
"download_flyer": "Baixar flyer"
"download_flyer": "Baixar flyer",
"address_description": "Qual é o endereço?",
"address_description_osm": "Qual é o endereço? (contribuidores do <a href='http://osm.org/copyright'>OpenStreetMap</a>)"
},
"confirm": {
"not_valid": "Algo deu errado.",
@ -231,7 +257,7 @@
"insert_your_address": "Informe seu endereço de e-mail",
"feed_description": "Para seguir as atualizações de um computador ou smartphone sem que você precise acessar essa página periodicamente, utilize um feeds RSS. </p>\n\n<p> Com feeds RSS você pode utilizar um app especial para receber atualizações de páginas que te interessam. É uma boa maneira de seguir muitas páginas rapidamente, sem a necessidade de criar contas de usuários ou outras complicações. </p>\n\n<li> Se você possui um Android, recomendamos <a href=\"https://f-droid.org/en/packages/net.frju.flym/\">Flym</a> ou Feeder </li>\n<li> Para iPhone / iPad você pode utilizar <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\"> Feed4U </a> </li>\n<li> Para desktop / laptop nós recomendamos Feedbro, que pode ser instalado no <a href=\"https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/\"> Firefox </a> ou <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\"> Chrome </a>. </li>\n<br/>\nAdicionado este link ao seu leitor de RSS irá mantê-lo atualizado.",
"ical_description": "Computadores e smartphones normalmente possuem aplicações de calendário capazes de importar um calendário remoto.",
"list_description": "Se você tem uma página e quer exibir uma lista de eventos, use o código seguinte",
"list_description": "Se você tem uma página e quer exibir uma lista de eventos, use o seguinte código",
"email_description": "Você pode obter os eventos que te interessam através de e-mail."
},
"oauth": {
@ -263,7 +289,7 @@
},
"setup": {
"https_warning": "Você está acessando por HTTP, lembre-se de alterar o valor de baseurl em config.json se você mudar para HTTPS!",
"completed": "Configuração completada",
"completed": "Configuração completa",
"start": "Iniciar",
"completed_description": "<p>Você pode agora autenticar-se com o seguinte usuário:<br/><br/>Usuário: <b>{email}</b><br/>Senha: <b>{password}<b/></p>",
"copy_password_dialog": "Sim, você precisa copiar a senha!"

333
locales/ru.json Normal file
View file

@ -0,0 +1,333 @@
{
"common": {
"send": "Отправить",
"where": "Где",
"address": "Адрес",
"when": "Когда",
"what": "Что",
"media": "Медиа",
"login": "Логин",
"email": "E-mail",
"password": "Пароль",
"register": "Зарегистрироваться",
"description": "Описание",
"remove": "Удалить",
"search": "Поиск",
"edit": "Ред.",
"info": "Инфо",
"add_event": "Доб. событие",
"hide": "Скрыть",
"confirm": "Подтвердить",
"admin": "Администратор",
"users": "Пользователи",
"events": "События",
"places": "Места",
"settings": "Опции",
"actions": "Действия",
"deactivate": "Выключить",
"remove_admin": "Удалить администратора",
"activate": "Активировать",
"save": "Сохранить",
"preview": "Предв. просмотр",
"logout": "Выйти",
"share": "Поделиться",
"name": "Имя",
"edit_event": "Ред. событие",
"related": "Связанные",
"add": "Доб.",
"logout_ok": "Вышел из системы",
"copy": "Копировать",
"recover_password": "Восстановить пароль",
"new_password": "Новый пароль",
"new_user": "Новый пользователь",
"ok": "Ок",
"cancel": "Отмена",
"enable": "Включить",
"disable": "Отключить",
"me": "Вы",
"password_updated": "Пароль изменен.",
"resources": "Ресурсы",
"n_resources": "нет ресурсов|ресурс|{n} ресурсы",
"activate_user": "Подтверждено",
"send_via_mail": "Отправить e-mail письмо",
"add_to_calendar": "Доб. в календарь",
"copied": "Скопировано",
"embed": "Встроить",
"embed_title": "Вставьте это событие на свой сайт",
"embed_help": "Скопируйте следующий код на свой сайт, и событие будет отображаться как здесь",
"feed": "RSS-канал",
"feed_url_copied": "Откройте скопированный URL канала в программе чтения RSS-каналов",
"follow_me_title": "Следите за обновлениями от fediverse",
"follow": "Подписаться",
"moderation": "Модерация",
"authorize": "Авторизация",
"filter": "Фильтр",
"start": "Начало",
"fediverse": "Fediverse",
"skip": "Пропустить",
"delete": "Удалить",
"import": "Импорт",
"max_events": "N. максимум событий",
"label": "Этикетка",
"close": "Закрыть",
"plugins": "Плагины",
"home": "Главная",
"content": "Содержание",
"admin_actions": "Действия админа",
"instances": "Экземпляры",
"title": "Название",
"set_password": "Установите пароль",
"copy_link": "Копировать ссылку",
"show_map": "Показать карту",
"about": "О сайте",
"event": "Событие",
"url": "URL",
"tags": "Теги",
"theme": "Тема",
"reset": "Сброс",
"collections": "Коллекции",
"help_translate": "Помогите перевести",
"displayname": "Отображаемое имя",
"federation": "Федерация",
"announcements": "Объявления",
"place": "Место",
"user": "Пользователь",
"pause": "Пауза",
"tag": "Тег",
"calendar": "Календарь",
"next": "След.",
"export": "Экспорт"
},
"login": {
"description": "Войдя в систему, вы можете публиковать новые события.",
"forgot_password": "Забыли пароль?",
"insert_email": "Введите свой адрес электронной почты",
"check_email": "Проверьте свой почтовый ящик и спам.",
"not_registered": "Не зарегистрированы?",
"error": "Не удалось войти в систему. Проверьте информацию для входа.",
"ok": "Вошел в систему"
},
"event": {
"each_2w": "Каждую вторую неделю",
"due": "до",
"from": "От",
"image_too_big": "Изображение не может быть больше 4 МБ",
"interact_with_me_at": "Общайтесь со мной на fediverse по адресу",
"each_month": "Каждый месяц",
"anon": "Анон",
"same_day": "в тот же день",
"what_description": "Название",
"description_description": "Описание",
"tag_description": "Тег",
"media_description": "Вы можете добавить флаер (необязательно)",
"added": "Событие добавлено",
"saved": "Событие сохранено",
"added_anon": "Событие добавлено, но еще не подтверждено.",
"updated": "Событие обновлено",
"where_description": "Где проходит мероприятие? Если его нет, вы можете его создать.",
"address_description": "Какой адрес?",
"address_description_osm": "Какой адрес? (<a href='http://osm.org/copyright'>OpenStreetMap</a> участники)",
"confirmed": "Событие одобрено",
"not_found": "Не удалось найти событие",
"remove_confirmation": "Вы уверены, что хотите удалить это событие?",
"recurrent": "Повторяющийся",
"edit_recurrent": "Ред. повтор. события:",
"show_recurrent": "повторяющ. события",
"show_past": "также предшеств. события",
"only_future": "только предстоящие события",
"recurrent_description": "Выберите частоту и выберите дни",
"multidate_description": "Это фестиваль? Выберите время начала и окончания",
"multidate": "Больше дней",
"normal": "Нормальный",
"normal_description": "Выберите день.",
"recurrent_1w_days": "Каждый {days}",
"interact_with_me": "Подпишись",
"remove_recurrent_confirmation": "Вы уверены, что хотите удалить это повторяющееся событие?\nPast events will be maintained, but no further events will be created.",
"import_URL": "Импорт из URL",
"import_ICS": "Импорт из ICS",
"ics": "ICS",
"alt_text_description": "Описание для людей с нарушениями зрения",
"choose_focal_point": "Выберите фокусную точку",
"remove_media_confirmation": "Вы подтверждаете удаление изображения?",
"download_flyer": "Скачать флаер",
"anon_description": "Вы можете добавить событие, не регистрируясь и не входя в систему, но вам придется подождать, пока кто-то его прочитает,\nподтверждение того, что это подходящее мероприятие. Изменить его будет невозможно.<br/><br/>\nВместо этого вы можете <a href='/login'>войти</a> или <a href='/register'>зарегистрироваться</a>. В противном случае действуйте и получите ответ как можно скорее. ",
"each_week": "Каждую неделю",
"follow_me_description": "Один из способов оставаться в курсе событий, публикуемых здесь на {title},\nследит за аккаунтом<u>{account}</u>из fediverse, например, через Mastodon, и, возможно, добавляет ресурсы в событие оттуда.<br/><br/>\nЕсли вы никогда не слышали о Mastodon и fediverse, рекомендуем прочитать <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>эта статья</a>.<br/><br/>Введите ниже свой экземпляр (например, mastodon.social)",
"import_description": "Вы можете импортировать события из других платформ и других инстанций с помощью стандартных форматов (ics и h-event)",
"recurrent_1m_ordinal": "{n} {days} каждого месяца",
"recurrent_2w_days": "Каждый второй {days}",
"recurrent_1m_days": "Каждое {days} число месяца"
},
"admin": {
"delete_resource_confirm": "Вы уверены, что хотите удалить этот ресурс?",
"delete_tag_confirm": "Вы уверены, что хотите удалить тег \"{tag}\"? Метка будет удалена из {n} событий.",
"block_user": "Заблок. пользователя",
"filter_instances": "Фильтр экземпляров",
"filter_users": "Фильтр пользователей",
"user_blocked": "Пользователь {user} заблокирован",
"user_block_confirm": "Вы уверены, что хотите заблокировать пользователя {user}?",
"enable_resources_help": "Позволяет добавлять ресурсы в событие из федерации",
"place_description": "Если вы ошиблись с местом или адресом, вы можете изменить его.<br/>Все текущие и прошлые события, связанные с этим местом, изменят адрес.",
"hide_boost_bookmark": "Скрывает буст/закладки",
"favicon": "Логотип",
"resources": "Ресурсы",
"disable_admin_user_confirm": "Вы уверены, что удалили права администратора у {user}?",
"allow_anon_event": "Разрешить анонимные мероприятия (должно быть подтверждение)?",
"recurrent_event_visible": "Показывать повторяющиеся события по умолчанию",
"select_instance_timezone": "Часовой пояс",
"event_confirm_description": "Вы можете подтвердить события, введенные анонимными пользователями, здесь",
"delete_user": "Удалить",
"remove_admin": "Удалить админа",
"disable_user_confirm": "Вы уверены, что хотите отключить {user}?",
"delete_user_confirm": "Вы уверены, что хотите удалить {user}?",
"enable_admin_user_confirm": "Вы уверены, добавить права администратора для {user}?",
"user_remove_ok": "Пользователь удален",
"user_create_ok": "Пользователь создан",
"event_remove_ok": "Событие удалено",
"allow_registration_description": "Разрешить открытую регистрацию?",
"allow_multidate_event": "Разрешить многодневные мероприятия",
"allow_recurrent_event": "Разрешить повторяющиеся события",
"allow_geolocation": "Разрешить геолокацию событий",
"federation": "",
"enable_federation": "Включить федерацию",
"enable_federation_help": "За этим экземпляром можно будет следить из федиверса",
"add_instance": "Доб. экземпляр",
"enable_resources": "Включить ресурсы",
"unblock": "Разблокировать",
"block": "Блокировать",
"user_add_help": "Новому пользователю будет отправлено электронное письмо с инструкциями по подтверждению подписки и выбору пароля",
"instance_name": "Имя экземпляра",
"show_resource": "Показать ресурс",
"hide_resource": "Скрыть ресурс",
"delete_resource": "Удалить ресурс",
"delete_announcement_confirm": "Вы уверены, что хотите удалить объявление?",
"instance_locale": "Язык по умолчанию",
"domain": "Домен",
"known_users": "Известные пользователи",
"delete_footer_link_confirm": "Вы хотите удалить эту ссылку?",
"new_collection": "Новая коллекция",
"default_images_help": "Вы должны <a href='/admin?tab=theme'>обновить</a> страницу, чтобы увидеть изменения.",
"delete_collection_confirm": "Вы уверены, что хотите удалить коллекцию <u>{collection}</u>?",
"geocoding_provider_type_help": "Программное обеспечение по умолчанию - Nominatim",
"created_at": "Создан в",
"instance_block_confirm": "Вы уверены, что хотите блокировать экземпляр {instance}?",
"announcement_remove_ok": "Объявление удалено",
"description_description": "Появляется в заголовке рядом с названием",
"trusted_instances_label_default": "Дружественные экземпляры",
"blocked": "Блокирован",
"geocoding_provider_type": "Программа для геокодирования",
"geocoding_provider": "Провайдер геокодирования",
"geocoding_provider_help": "Поставщиком по умолчанию является Nominatim",
"geolocation": "Геолокация",
"title_description": "Он используется в заголовке страницы, в теме письма для экспорта RSS- и ICS-каналов.",
"instance_place": "Ориентировочное местонахождение данного экземпляра",
"instance_name_help": "Аккаунт ActivityPub, для подписки",
"enable_trusted_instances": "Включите дружественные экземпляры",
"trusted_instances_label": "Навигационная метка для дружественных экземпляров",
"trusted_instances_label_help": "Метка по умолчанию в 'Дружеств. экземплярах'",
"add_trusted_instance": "Доб. дружественный экземпляр",
"instance_place_help": "Метка для отображения в других экземплярах",
"is_dark": "Темная тема",
"add_link": "Добавить ссылку",
"footer_links": "Ссылки в нижнем колонтитуле",
"edit_place": "Ред. место",
"edit_tag": "Ред. тег",
"new_announcement": "Новое объявление",
"show_smtp_setup": "Настройки электронной почты",
"smtp_hostname": "Имя хоста SMTP",
"smtp_port": "Порт SMTP",
"smtp_test_success": "Тестовое письмо отправлено на {admin_email}, пожалуйста, проверьте свой почтовый ящик",
"smtp_test_button": "Отправьте тестовое электронное письмо",
"smtp_use_sendmail": "Используйте sendmail",
"admin_email": "Электронная почта администратора",
"admin_email_help": "Адрес, который мы используем в качестве отправителя для отправки электронных писем. Это также адрес, на который отправляются электронные письма администраторов",
"widget": "Виджет",
"wrong_domain_warning": "Baseurl, настроенный в config.json <b>({baseurl})</b> отличается от того, который вы посещаете <b>({url})</b>",
"collections_description": "Коллекции - это группировка событий по тегам и местам. Они будут отображаться на главной странице",
"edit_collection": "Ред. коллекцию",
"config_plugin": "Конфигурация плагина",
"header_image": "Изображение заголовка",
"hide_thumbs": "Скрыть превью",
"hide_calendar": "Скрыть календарь",
"default_images": "Изображения по умолчанию",
"geocoding_countrycodes": "Коды стран",
"geocoding_countrycodes_help": "Позволяет установить фильтр для поиска на основе кодов городов",
"geocoding_test_button": "Тест геокодирования",
"geocoding_test_success": "Служба геокодирования на {service_name} работает",
"geocoding_test_error": "Служба геокодирования недоступна по адресу {service_name}",
"tilelayer_provider_attribution": "Атрибуция",
"announcement_description": "В этом разделе вы можете вставлять объявления, которые будут оставаться на главной странице",
"delete_trusted_instance_confirm": "Вы действительно хотите удалить этот пункт из меню дружественных экземпляров?",
"edit_tag_help": "Вы можете изменить тег, заменив его новым или объединив с существующим. Связанные с ним {n} события также будут изменены.",
"instance_timezone_description": "Gancio предназначен для сбора событий определенного места, например, города. Все события в этом месте будут отображаться в выбранном для него часовом поясе.",
"instance_locale_description": "Предпочитаемый язык пользователя для страниц. Иногда сообщения должны отображаться на одном языке для всех (например, при публикации через ActivityPub или при отправке некоторых электронных писем). В этих случаях будет использоваться язык, выбранный выше.",
"trusted_instances_help": "Список дружественных экземпляров будет показан в заголовке",
"smtp_description": "<ul><li>Администратор должен получать электронное письмо при добавлении события анонса (если включено).</li><li>Администратор должен получить электронное письмо с запросом на регистрацию (если включено).</li><li>Пользователь должен получить электронное письмо с запросом на регистрацию.</li><li>Пользователь должен получить электронное письмо с подтверждением регистрации.</li><li>Пользователь должен получить подтверждение по электронной почте при подписке непосредственно администратором.</li><li>Пользователи должны получать электронное письмо для восстановления пароля, если они его забыли</li></ul>",
"hide_boost_bookmark_help": "Скрывает маленькие иконки, показывающие количество бустов и закладок, поступающих из fediverse"
},
"recover": {
"not_valid_code": "Что-то пошло не так."
},
"export": {
"email_description": "Вы можете получать интересующие вас события на электронной почте.",
"insert_your_address": "Введите свой адрес электронной почты",
"ical_description": "Компьютеры и смартфоны обычно оснащены приложением календаря, способным импортировать календарь (ical файл).",
"list_description": "Если у вас есть веб-сайт и вы хотите показать список событий, используйте следующий код",
"intro": "В отличие от несоциальных платформ, которые делают все, чтобы удержать пользователей и данные о них, мы считаем, что информация, как и люди, должна быть свободной. Для этого вы можете оставаться в курсе нужных вам событий, без обязательного посещения этого сайта.",
"feed_description": "Чтобы следить за обновлениями с компьютера или смартфона без необходимости периодически открывать этот сайт, используйте RSS-каналы.\n\n<p> С помощью RSS-каналов вы используете специальное приложение для получения обновлений с интересующих вас сайтов. Это хороший способ быстро следить за многими сайтами, без необходимости создавать учетную запись или других сложностей. </p>\n\n<li> Если у вас Android, мы рекомендуем <a href=\"https://f-droid.org/en/packages/net.frju.flym/\">Flym</a> или Feeder </li>\n<li> Для iPhone / iPad вы можете использовать <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\"> Feed4U </a> </li>\n<li> Для настольных компьютеров / ноутбуков мы рекомендуем Feedbro, который должен быть установлен на <a href=\"https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/\"> Firefox </a> или <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\"> Chrome </a>. </li>\n<br/>\nДобавив эту ссылку в свой RSS-канал, вы всегда будете в курсе последних событий."
},
"register": {
"error": "Ошибка: ",
"complete": "Регистрация должна быть подтверждена.",
"first_user": "Администратор создан",
"description": "Общественные движения должны организовываться и самофинансироваться.<br/>\n<br/>Перед публикацией, <strong> аккаунт должен быть подтвержден</strong>, подумайте о том, что <strong> на этом сайте вы найдете реальных людей</strong>, поэтому напишите две строчки, чтобы сообщить нам, какие события вы хотели бы опубликовать."
},
"setup": {
"copy_password_dialog": "Да, вы можете скопировать пароль!",
"https_warning": "Вы посещаете сайт с HTTP, не забудьте изменить baseurl в config.json, если вы перейдете на HTTPS!",
"start": "Начало",
"completed_description": "<p>Вы можете войти в систему под следующим пользователем:<br/><br/>Логин: <b>{email}</b><br/>Пароль: <b>{password}<b/></p>",
"completed": "Установка завершена"
},
"confirm": {
"title": "Подтверждение пользователя",
"not_valid": "Что-то пошло не так.",
"valid": "Ваша учетная запись подтверждена, теперь вы можете <a href=\"/login\">войти</a>"
},
"settings": {
"password_updated": "Пароль изменен.",
"update_confirm": "Хотите ли вы сохранить свою изменение?",
"change_password": "Изменить пароль",
"danger_section": "Опасная опция",
"remove_account": "При нажатии следующей кнопки ваша учетная запись пользователя будет удалена. Опубликованные вами события не будут удалены.",
"remove_account_confirm": "Вы собираетесь навсегда удалить свой аккаунт"
},
"error": {
"email_taken": "Эта электронная почта уже используется.",
"nick_taken": "Это имя пользователя уже используется."
},
"auth": {
"not_confirmed": "Не подтверждено…",
"fail": "Не удалось войти в систему. Вы уверены, что пароль правильный?"
},
"validators": {
"required": "{fieldName} заполните",
"email": "Доб. действующий адрес электронной почты"
},
"oauth": {
"authorization_request": "Приложение <code>{app}</code> запрашивает следующую авторизацию на <code>{instance_name}</code>:",
"scopes": {
"event:write": "Добавляйте и редактируйте свои события"
},
"redirected_to": "После подтверждения вы будете перенаправлены на <code>{url}</code>"
},
"about": "\n <p><a href='https://gancio.org'>Gancio</a> это афиша для местных сообществ.</p>\n ",
"ordinal": {
"1": "первый",
"2": "второй",
"3": "третий",
"4": "четвертый",
"5": "пятый",
"-1": "последний"
}
}

View file

@ -1,293 +0,0 @@
{
"common": {
"embed_help": "将以下代码复制并粘贴到你的网站,此事件将像这样显示",
"follow": "关注",
"add_event": "添加事件",
"send": "发送",
"where": "地点",
"address": "地址",
"when": "时间",
"what": "事件",
"media": "媒体",
"login": "登录",
"email": "电子邮箱",
"register": "注册",
"description": "描述",
"hide": "隐藏",
"search": "搜索",
"confirm": "确认",
"admin": "管理员",
"users": "用户",
"events": "事件",
"actions": "操作",
"deactivate": "取消",
"remove_admin": "移除管理员",
"activate": "激活",
"save": "该村",
"logout": "登出",
"name": "名称",
"associate": "合作者",
"add": "添加",
"recover_password": "重置密码",
"enable": "启用",
"me": "你",
"ok": "完成",
"resources": "资源",
"n_resources": "无资源|1 个资源|{n} 个资源",
"displayname": "显示名称",
"copy_link": "复制链接",
"send_via_mail": "发送电子邮件",
"embed": "嵌入式页面",
"feed_url_copied": "在你的 RSS 阅读器中打开复制的链接",
"follow_me_title": "在 Fediverse 网络中关注更新",
"feed": "RSS 源",
"moderation": "中等",
"authorize": "认证",
"title": "标题",
"filter": "筛选",
"pause": "暂停",
"start": "开始",
"announcements": "公告",
"url": "URL",
"place": "地点",
"theme": "主题",
"label": "标签",
"collections": "收藏",
"max_events": "最大事件数",
"next": "下一个",
"export": "导出",
"remove": "移除",
"settings": "选项",
"logout_ok": "已登出",
"new_password": "新密码",
"new_user": "新用户",
"places": "地点",
"edit": "编辑",
"cancel": "取消",
"password": "密码",
"info": "信息",
"preview": "预览",
"share": "分享",
"edit_event": "编辑事件",
"copy": "复制",
"related": "相关",
"set_password": "设置密码",
"instances": "实例",
"activate_user": "已确认",
"federation": "联盟",
"add_to_calendar": "添加到日历",
"copied": "已复制",
"embed_title": "在你的网页上嵌入此事件",
"user": "用户",
"event": "事件",
"fediverse": "Fediverse 网络",
"skip": "跳过",
"delete": "移除",
"import": "导入",
"tags": "标签",
"close": "关闭",
"disable": "禁用",
"password_updated": "密码已修改。",
"reset": "重置"
},
"export": {
"list_description": "如果你有一个网站并希望展示一个事件列表,使用以下代码",
"email_description": "你可以通过发给你的电子邮件了解你感兴趣的事件。",
"insert_your_address": "输入你的电子邮箱地址",
"ical_description": "电脑和智能手机通常预装了能够导入远程日历的日历应用。",
"intro": "与那些竭尽全力保留用户和数据的非社交平台不同,我们认为信息和人一样,都必须是自由的。因此,即使不通过此网站,你仍可随时了解你想了解的事件之最新情况。",
"feed_description": "要想从电脑或智能手机上关注更新而无需定期打开本网站请使用RSS订阅。</p>\n\n<p>通过RSS订阅您可以使用一个特殊的应用程序来接收你感兴趣的网站的更新。这是一个快速关注许多网站的好方法不需要创建账户或做其他事。</p>\n\n<li> 如果您使用 Android我们推荐 <a href=\"https://f-droid.org/en/packages/net.frju.flym/\">Flym</a> 或 Feeder。</li>\n<li> 对于iPhone/iPad您可以使用 <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\">Feed4U</a> </li>\n<li> 对于台式机/笔记本电脑,我们推荐可在 <a href=\"https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/\">Firefox</a> 或 <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\">Chrome</a> 上安装的 Feedbro。</li>\n<br/>\n将此链接添加到你的RSS阅读器中以获得最新信息。"
},
"register": {
"description": "社会运动应该组织起来,并自筹资金<br/>\n<br/>在你能发布内容之前,<strong>你的账户必须审核通过</strong>,考虑到<strong>你可以通过此网站发现现实中的人</strong>,请写一些东西告诉我们你希望发布什么。",
"error": "错误: ",
"complete": "注册必须经过确认。",
"first_user": "管理员已创建"
},
"event": {
"anon_description": "即使不登录或注册,你也可以创建事件,但必须等待一些人看到它,\n并确认这是一个合适的事件。此外你也不能够修改它。<br/><br/>\n你也可以 <a href='/login'>登录</a> 或 <a href='/register'>注册</a>。或者继续浏览以得到答案。 ",
"anon": "匿名",
"same_day": "在同一天",
"what_description": "标题",
"description_description": "描述",
"added": "事件已添加",
"saved": "事件已保存",
"added_anon": "事件已添加,等待确认。",
"updated": "事件已更新",
"where_description": "事件的地点在哪里?如果不存在,你可以创建一个。",
"confirmed": "事件已确认",
"not_found": "找不到事件",
"recurrent": "日常事件",
"edit_recurrent": "编辑日常事件:",
"show_recurrent": "日常事件",
"show_past": "以及过往的事件",
"multidate_description": "这是一个节日吗?选择它开始和结束的时间",
"multidate": "更多日期",
"normal_description": "选择日期。",
"recurrent_2w_days": "每 {days} 天一次",
"each_week": "每周",
"each_2w": "隔周一次",
"due": "直到",
"from": "来自",
"image_too_big": "图片不能大于 4MB",
"interact_with_me_at": "在 Fediverse 网络上与我互动",
"interact_with_me": "关注我",
"remove_recurrent_confirmation": "你确定要移除这个日常事件吗?\n过去的事件仍将被维护但不会再添加新事件。",
"import_URL": "从 URL 导入",
"import_ICS": "从 ICS 导入",
"ics": "ICS",
"alt_text_description": "为视觉障碍者提供的说明",
"choose_focal_point": "选择联络点",
"download_flyer": "下载传单",
"tag_description": "标签",
"media_description": "你可以添加一份传单(可选)",
"recurrent_description": "选择频率和日期",
"only_future": "仅限即将到来的事件",
"normal": "普通",
"recurrent_1w_days": "每 {days} 天",
"recurrent_1m_days": "|每月的第 {days} 天|每月的第 {days} 天",
"recurrent_1m_ordinal": "每月的第 {n} 个 {days}",
"each_month": "每月",
"follow_me_description": "一种对这里发布的 {title} 事件保持关注的方法,\n是在 Fediverse 网络,比如 Mastodon上关注 <u>{account}</u>,亦有可能通过此方式给此事件添加资源。<br/><br/>\n如果你没听说过 Mastodon 和 Fediverse我们建议你阅读 <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>这篇文章</a>。<br/><br/>在下方输入你的实例名称例如mastodon.social",
"import_description": "你可以从其他平台和实例通过标准格式ICS 和 hCalendar导入事件",
"remove_media_confirmation": "你确认要删除图片吗?",
"remove_confirmation": "你确定要移除此事件吗?"
},
"login": {
"check_email": "检查你的电子邮箱收件箱和垃圾邮件箱。",
"not_registered": "还未注册?",
"forgot_password": "忘记密码了?",
"insert_email": "输入你的电子邮箱地址",
"error": "无法登录,检查你的登录信息。",
"ok": "已登录",
"description": "登录以发布新事件。"
},
"recover": {
"not_valid_code": "发生了一些错误。"
},
"admin": {
"delete_user": "移除",
"remove_admin": "移除管理员",
"disable_user_confirm": "你确定禁用 {user} 吗?",
"disable_admin_user_confirm": "你确定移除 {user} 的管理员权限吗?",
"user_remove_ok": "用户已移除",
"user_create_ok": "用户已创建",
"event_remove_ok": "事件已移除",
"allow_registration_description": "允许公众注册?",
"allow_anon_event": "允许发布匿名事件(需要确认)?",
"allow_recurrent_event": "允许日常事件",
"federation": "联邦社交网络 / ActivityPub",
"enable_federation": "启用联邦社交网络",
"add_instance": "添加实例",
"select_instance_timezone": "时区",
"enable_resources": "启用资源",
"hide_boost_bookmark": "隐藏助力/书签",
"block": "屏蔽",
"unblock": "解除屏蔽",
"instance_name": "实例名称",
"hide_resource": "隐藏资源",
"delete_resource_confirm": "你确定要删除此资源吗?",
"filter_instances": "筛选实例",
"resources": "资源",
"favicon": "图标",
"user_block_confirm": "你确定要屏蔽用户 {user} 吗?",
"delete_announcement_confirm": "你确定要移除这个公告吗?",
"announcement_remove_ok": "公告已移除",
"instance_locale": "默认语言",
"title_description": "这将被用作页面的标题和电子邮件的主题,以导出 RSS 和 ICS 源。",
"description_description": "在标题旁的页眉中显示",
"instance_place_help": "在其他的实例中显示的标签",
"delete_trusted_instance_confirm": "你确定要从友好实例菜单中删除此项目吗?",
"edit_place": "编辑地点",
"new_announcement": "新公告",
"show_smtp_setup": "电子邮件设置",
"smtp_port": "SMTP 端口",
"smtp_secure": "SMTP 安全协议TLS 或 STARTTLS",
"smtp_test_success": "一封测试邮件已发送至 {admin_mail},请检查你的收件箱",
"sender_email": "发件人",
"widget": "小组件",
"wrong_domain_warning": "在 config.json 中设置的 baseurl <b>{baseurl}</b> 与你正在访问的 <b>{url}</b> 不同",
"edit_collection": "编辑收藏",
"event_confirm_description": "你可以在此确认匿名用户提交的事件",
"recurrent_event_visible": "默认显示日常事件",
"place_description": "如果你弄错了地点或地址,你可以修改。<br/>与这个地点相关的当前和过去的所有事件都会改变地址。",
"delete_user_confirm": "你确定移除 {user} 吗?",
"enable_admin_user_confirm": "你确定授予 {user} 管理员权限吗",
"enable_federation_help": "这将允许从 Fediverse 上关注此实例",
"enable_resources_help": "允许从 Fediverse 为此事件添加资源",
"hide_boost_bookmark_help": "隐藏来自 Fediverse 上的助力和书签数量图标",
"block_user": "屏蔽用户",
"filter_users": "筛选用户",
"user_add_help": "一封带有确认订阅和设置密码指引的邮件将被发送给新用户",
"show_resource": "显示资源",
"delete_resource": "删除资源",
"user_blocked": "用户 {user} 已屏蔽",
"instance_block_confirm": "你确定要屏蔽实例 {instance} 吗?",
"announcement_description": "你可以在此段落插入显示于首页的公告",
"instance_timezone_description": "Gancio 被设计用来收集特定区域,比如一座城市所发生的事件。这里的所有事件将以所选择的时区显示。",
"instance_locale_description": "特定页面偏好的用户语言。有时信息必须以相同的语言对所有人显示(比如通过 ActivityPub 发布内容或发送一些电子邮件时)。在这种情况下将使用上面选择的语言。",
"instance_place": "该实例的指示性位置",
"trusted_instances_help": "友好实例的列表将被显示于页眉",
"footer_links": "页脚链接",
"smtp_description": "<ul><li>当匿名事件被添加时(如果启用),管理员应当收到邮件</li>管理员应当会受到注册请求邮件(如果启用)。<li></li><li>用户应当会受到注册请求邮件。</li><li>用户应当收到注册确认邮件。</li><li>当管理员直接订阅时,用户应当受到邮件。</li><li>用户忘记密码时应当收到密码重置邮件。</li></ul>",
"smtp_use_sendmail": "使用 sendmail",
"smtp_test_button": "发送测试邮件",
"new_collection": "新建收藏",
"collections_description": "收藏是按标签和地点分组的事件。它们将于主页上显示",
"enable_trusted_instances": "启用友好实例",
"add_trusted_instance": "添加一个友好实例",
"add_link": "添加链接",
"delete_footer_link_confirm": "确定移除此链接吗?",
"instance_name_help": "要关注的 ActivityPub 账号",
"is_dark": "暗色主题",
"smtp_hostname": "SMTP 主机名"
},
"auth": {
"fail": "无法登录。你确定密码正确吗?",
"not_confirmed": "尚未确认……"
},
"settings": {
"change_password": "修改密码",
"password_updated": "密码已修改。",
"remove_account_confirm": "你即将永久删除你的账号",
"update_confirm": "你希望保存你的修改吗?",
"remove_account": "按下下方的按钮后你的账号将被删除。你发布的事件不会删除。",
"danger_section": "危险段落"
},
"error": {
"email_taken": "此电子邮箱地址已被使用。",
"nick_taken": "此昵称已被使用。"
},
"confirm": {
"title": "用户确认",
"not_valid": "出现了一些错误。",
"valid": "你的账户已被确认,你现在可以 <a href=\"/login\">登录</a>"
},
"ordinal": {
"4": "第四",
"5": "第五",
"2": "第二",
"-1": "最后",
"1": "第一",
"3": "第三"
},
"validators": {
"required": "{fieldName} 是必填项",
"email": "输入有效的电子邮箱地址"
},
"oauth": {
"authorization_request": "应用 <code>{app}</code> 申请在 <code>{instance_name}</code> 上获得以下权限:",
"redirected_to": "在确认后你将被重定向到 <code>{url}</code>",
"scopes": {
"event:write": "添加与编辑你的事件"
}
},
"setup": {
"completed_description": "<p>你现在可以以以下用户登录<br/><br/>用户名:<b>{email}</b><br/>密码:<b>{password}<b/></p>",
"copy_password_dialog": "没错,你必须复制密码!",
"start": "开始",
"https_warning": "你正在使用 HTTP 访问,如果你切换到 HTTPS记得在 config.json 中修改 baseurl",
"completed": "安装完成"
},
"about": "\n <p><a href='https://gancio.org'>Gancio</a> 是为本地社区设计的的共享日程表。</p>\n "
}

View file

@ -2,7 +2,7 @@ const config = require('./server/config.js')
const minifyTheme = require('minify-css-string').default
const locales = require('./locales/index')
import { ca, de, en, es, eu, fr, gl, it, nb, pl, pt, sk, zhHans } from 'vuetify/lib/locale'
import { ca, de, en, es, eu, fr, gl, it, nb, pl, pt, sk, ru, zhHans } from 'vuetify/lib/locale'
const isDev = (process.env.NODE_ENV !== 'production')
module.exports = {
@ -142,7 +142,7 @@ module.exports = {
},
buildModules: ['@nuxtjs/vuetify'],
vuetify: {
lang: { locales: { ca, de, en, es, eu, fr, gl, it, nb, pl, pt, sk, zhHans } },
lang: { locales: { ca, de, en, es, eu, fr, gl, it, nb, pl, pt, sk, ru, zhHans } },
treeShake: true,
theme: {
options: {

View file

@ -1,15 +1,15 @@
{
"name": "gancio",
"version": "1.6.0-rc2",
"version": "1.6.2",
"description": "A shared agenda for local communities",
"author": "lesion",
"scripts": {
"build": "nuxt build --modern",
"start:inspect": "NODE_ENV=production node --inspect node_modules/.bin/nuxt start --modern",
"dev": "nuxt dev",
"test-sqlite": "export NODE_ENV=test; export DB=sqlite; jest --bail=1",
"test-mariadb": "export NODE_ENV=test; export DB=mariadb; jest --bail=1",
"test-postgresql": "export NODE_ENV=test; export DB=postgresql; jest --bail=1",
"test-sqlite": "export NODE_ENV=test; export DB=sqlite; jest --testEnvironment=jest-environment-node --bail=1",
"test-mariadb": "export NODE_ENV=test; export DB=mariadb; jest --testEnvironment=jest-environment-node --bail=1",
"test-postgresql": "export NODE_ENV=test; export DB=postgresql; jest --testEnvironment=jest-environment-node --bail=1",
"start": "nuxt start --modern",
"doc": "cd docs && bundle exec jekyll b",
"doc:dev": "cd docs && bundle exec jekyll s --drafts",
@ -34,7 +34,7 @@
"node": ">=14 <=16"
},
"dependencies": {
"@mdi/js": "^7.0.96",
"@mdi/js": "^7.1.96",
"@nuxtjs/auth": "^4.9.1",
"@nuxtjs/axios": "^5.13.5",
"@nuxtjs/i18n": "^7.3.0",
@ -47,15 +47,15 @@
"cookie-session": "^2.0.0",
"cookie-universal-nuxt": "^2.2.2",
"cors": "^2.8.5",
"dayjs": "^1.11.5",
"dayjs": "^1.11.7",
"dompurify": "^2.4.1",
"email-templates": "^10.0.1",
"express": "^4.18.1",
"express-rate-limit": "^6.7.0",
"http-signature": "^1.3.6",
"https-proxy-agent": "^5.0.1",
"ical.js": "^1.5.0",
"ics": "^2.40.0",
"jest-environment-jsdom": "^29.3.1",
"jsdom": "^20.0.2",
"jsonwebtoken": "^8.5.1",
"leaflet": "^1.9.2",
@ -63,6 +63,7 @@
"linkifyjs": "4.0.2",
"lodash": "^4.17.21",
"mariadb": "^3.0.1",
"memory-cache": "^0.2.0",
"microformat-node": "^2.0.1",
"minify-css-string": "^1.0.0",
"mkdirp": "^1.0.4",
@ -78,17 +79,17 @@
"passport-oauth2-client-password": "^0.1.2",
"passport-oauth2-client-public": "^0.0.1",
"pg": "^8.8.0",
"sequelize": "^6.25.6",
"sequelize": "^6.28.0",
"sequelize-slugify": "^1.6.2",
"sharp": "^0.27.2",
"sqlite3": "^5.0.11",
"sqlite3": "^5.1.4",
"telegraf": "^4.9.1",
"tiptap": "^1.32.0",
"tiptap-extensions": "^1.35.0",
"umzug": "^2.3.0",
"v-calendar": "^2.4.1",
"vue2-leaflet": "^2.7.1",
"vuetify": "2.6.12",
"vuetify": "2.6.13",
"winston": "^3.8.2",
"winston-daily-rotate-file": "^4.7.1",
"yargs": "^17.5.0"
@ -96,12 +97,13 @@
"devDependencies": {
"@nuxtjs/vuetify": "^1.12.3",
"jest": "^29.3.1",
"prettier": "^2.7.1",
"jest-environment-node": "^29.3.1",
"prettier": "^2.8.1",
"pug": "^3.0.2",
"pug-plain-loader": "^1.1.0",
"sass": "^1.56.1",
"sass": "^1.56.2",
"sequelize-cli": "^6.3.0",
"supertest": "^6.2.4",
"supertest": "^6.3.3",
"webpack": "4",
"webpack-cli": "^4.10.0"
},

View file

@ -26,6 +26,18 @@ v-container.container.pa-0.pa-md-3
v-tab-item(value='places')
Places
//- TAGS
v-tab(href='#tags') {{$t('common.tags')}}
v-tab-item(value='tags')
Tags
//- GEOCODING / MAPS
v-tab(href='#geolocation' v-if='settings.allow_geolocation') {{$t('admin.geolocation')}}
v-tab-item(value='geolocation')
client-only(placeholder='Loading...')
Geolocation
//- Collections
v-tab(href='#collections') {{$t('common.collections')}}
v-tab-item(value='collections')
@ -70,7 +82,9 @@ export default {
Users: () => import(/* webpackChunkName: "admin" */'../components/admin/Users'),
Events: () => import(/* webpackChunkName: "admin" */'../components/admin/Events'),
Places: () => import(/* webpackChunkName: "admin" */'../components/admin/Places'),
Tags: () => import(/* webpackChunkName: "admin" */'../components/admin/Tags'),
Collections: () => import(/* webpackChunkName: "admin" */'../components/admin/Collections'),
[process.client && 'Geolocation']: () => import(/* webpackChunkName: "admin" */'../components/admin/Geolocation.vue'),
Federation: () => import(/* webpackChunkName: "admin" */'../components/admin/Federation.vue'),
Moderation: () => import(/* webpackChunkName: "admin" */'../components/admin/Moderation.vue'),
Plugin: () => import(/* webpackChunkName: "admin" */'../components/admin/Plugin.vue'),

View file

@ -91,17 +91,33 @@ export default {
WhereInput,
DateInput
},
validate({ store }) {
return (store.state.auth.loggedIn || store.state.settings.allow_anon_event)
validate({ store, params, error }) {
// should we allow anon event?
if(!store.state.settings.allow_anon_event && !store.state.auth.loggedIn) {
return error({ statusCode: 401, message: 'Not allowed'})
}
// do not allow edit to anon users
if (params.edit && !store.state.auth.loggedIn) {
return error({ statusCode: 401, message: 'Not allowed'})
}
return true
},
async asyncData({ params, $axios, error }) {
async asyncData({ params, $axios, error, $auth, store }) {
if (params.edit) {
const data = { event: { place: {}, media: [] } }
data.id = params.edit
data.edit = true
let event
try {
event = await $axios.$get('/event/' + data.id)
event = await $axios.$get('/event/detail/' + data.id)
if (!$auth.user.is_admin && $auth.user.id !== event.userId) {
error({ statusCode: 401, message: 'Not allowed' })
return {}
}
} catch (e) {
error({ statusCode: 404, message: 'Event not found!' })
return {}
@ -228,6 +244,8 @@ export default {
if (this.date.dueHour) {
[hour, minute] = this.date.dueHour.split(':')
formData.append('end_datetime', dayjs(this.date.due).hour(Number(hour)).minute(Number(minute)).second(0).unix())
} else if (!!this.date.multidate) {
formData.append('end_datetime', dayjs(this.date.due).hour(24).minute(0).second(0).unix())
}
if (this.edit) {

View file

@ -31,7 +31,7 @@ export default {
async asyncData ({ $axios, params, error }) {
try {
const collection = params.collection
const events = await $axios.$get(`/collections/${collection}`)
const events = await $axios.$get(`/collections/${encodeURIComponent(collection)}`)
return { events, collection }
} catch (e) {
console.error(e)

View file

@ -17,7 +17,7 @@ export default {
layout: 'iframe',
async asyncData ({ $axios, params, error }) {
try {
const event = await $axios.$get(`/event/${params.event_id}`)
const event = await $axios.$get(`/event/detail/${params.event_id}`)
return { event }
} catch (e) {
error({ statusCode: 404, message: 'Event not found' })

View file

@ -3,7 +3,7 @@ v-container#event.pa-0.pa-sm-2
//- EVENT PAGE
//- gancio supports microformats (http://microformats.org/wiki/h-event)
//- and microdata https://schema.org/Event
v-card.h-event(itemscope itemtype="https://schema.org/Event")
v-card.h-event(itemscope itemtype="https://schema.org/Event" v-touch="{ left: goNext, right: goPrev }")
v-card-text
v-row
@ -28,13 +28,13 @@ v-container#event.pa-0.pa-sm-2
.text-h6.p-location.h-adr(itemprop="location" itemscope itemtype="https://schema.org/Place")
v-icon(v-text='mdiMapMarker' small)
nuxt-link.vcard.ml-2.p-name.text-decoration-none.text-button(itemprop="name" :to='`/place/${event.place.name}`') {{event.place && event.place.name}}
nuxt-link.vcard.ml-2.p-name.text-decoration-none.text-button(itemprop="name" :to='`/place/${encodeURIComponent(event.place.name)}`') {{event.place && event.place.name}}
.text-caption.p-street-address(itemprop='address') {{event.place && event.place.address}}
//- tags, hashtags
v-card-text.pt-0(v-if='event.tags && event.tags.length')
v-chip.p-category.ml-1.mt-1(v-for='tag in event.tags' small label color='primary'
outlined :key='tag' :to='`/tag/${tag}`') {{tag}}
outlined :key='tag' :to='`/tag/${encodeURIComponent(tag)}`') {{tag}}
v-divider
//- info & actions
@ -185,7 +185,7 @@ export default {
},
async asyncData ({ $axios, params, error }) {
try {
const event = await $axios.$get(`/event/${params.slug}`)
const event = await $axios.$get(`/event/detail/${params.slug}`)
return { event }
} catch (e) {
error({ statusCode: 404, message: 'Event not found' })
@ -318,12 +318,22 @@ export default {
keyDown (ev) {
if (ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey) { return }
if (ev.key === 'ArrowRight' && this.event.next) {
this.$router.replace(`/event/${this.event.next}`)
this.goNext()
}
if (ev.key === 'ArrowLeft' && this.event.prev) {
this.goPrev()
}
},
goPrev () {
if (this.event.prev) {
this.$router.replace(`/event/${this.event.prev}`)
}
},
goNext () {
if (this.event.next) {
this.$router.replace(`/event/${this.event.next}`)
}
},
showResource (resource) {
this.showResources = true
this.selectedResource = resource

View file

@ -27,8 +27,11 @@ export default {
name: 'Index',
components: { Event, Announcement, ThemeView },
middleware: 'setup',
async fetch () {
return this.getEvents()
fetch () {
return this.getEvents({
start: this.start,
end: this.end
})
},
activated() {
if (this.$fetchState.timestamp <= Date.now() - 60000) {
@ -40,13 +43,11 @@ export default {
mdiMagnify, mdiCloseCircle,
isCurrentMonth: true,
now: dayjs().unix(),
date: dayjs.tz().format('YYYY-MM-DD'),
start: dayjs().startOf('month').unix(),
end: null,
searching: false,
tmpEvents: [],
selectedDay: null,
show_recurrent: $store.state.settings.recurrent_event_visible,
storeUnsubscribe: null,
reload_events: 0
}
},
@ -69,76 +70,77 @@ export default {
}
},
computed: {
...mapState(['settings', 'announcements', 'events']),
...mapState(['settings', 'announcements', 'events', 'filter']),
visibleEvents () {
if (this.searching) {
if (this.filter.query && this.filter.query.length > 2) {
return this.tmpEvents
}
const now = dayjs().unix()
if (this.selectedDay) {
const min = dayjs.tz(this.selectedDay).startOf('day').unix()
const max = dayjs.tz(this.selectedDay).endOf('day').unix()
return this.events.filter(e => (e.start_datetime <= max && (e.end_datetime || e.start_datetime) >= min) && (this.show_recurrent || !e.parentId))
return this.events.filter(e => (e.start_datetime <= max && (e.end_datetime || e.start_datetime) >= min) && (this.filter.show_recurrent || !e.parentId))
} else if (this.isCurrentMonth) {
return this.events.filter(e => ((e.end_datetime ? e.end_datetime > now : e.start_datetime + 3 * 60 * 60 > now) && (this.show_recurrent || !e.parentId)))
return this.events.filter(e => ((e.end_datetime ? e.end_datetime > now : e.start_datetime + 3 * 60 * 60 > now) && (this.filter.show_recurrent || !e.parentId)))
} else {
return this.events.filter(e => this.show_recurrent || !e.parentId)
return this.events.filter(e => this.filter.show_recurrent || !e.parentId)
}
}
},
created () {
this.$root.$on('dayclick', this.dayChange)
this.$root.$on('monthchange', this.monthChange)
this.$root.$on('search', debounce(this.search, 100))
this.$root.$on('layout_loaded', () => {
this.reload_events++
})
this.storeUnsubscribe = this.$store.subscribeAction( { after: (action, state) => {
if (action.type === 'setFilter') {
if (this.filter.query && this.filter.query.length > 2) {
this.search()
} else {
this.tmpEvents = []
this.$fetch()
}
}
}})
// this.$root.$on('search', debounce(this.search, 100))
// this.$root.$on('layout_loaded', () => {
// this.reload_events++
// })
},
destroyed () {
this.$root.$off('dayclick')
this.$root.$off('monthchange')
this.$root.$off('search')
if (typeof this.storeUnsubscribe === 'function') {
this.storeUnsubscribe()
}
},
methods: {
...mapActions(['getEvents']),
async search (query) {
if (query) {
this.tmpEvents = await this.$axios.$get(`/event/search?search=${query}`)
this.searching = true
} else {
this.tmpEvents = null
this.searching = false
}
},
updateEvents () {
return this.getEvents({
start: this.start,
end: this.end,
show_recurrent: true
search: debounce(async function() {
this.tmpEvents = await this.$api.getEvents({
start: 0,
show_recurrent: this.filter.show_recurrent,
show_multidate: this.filter.show_multidate,
query: this.filter.query
})
},
}, 200),
async monthChange ({ year, month }) {
this.$nuxt.$loading.start()
this.$nextTick( async () => {
let isCurrentMonth
// unselect current selected day
this.selectedDay = null
// check if current month is selected
if (month - 1 === dayjs.tz().month() && year === dayjs.tz().year()) {
this.isCurrentMonth = true
isCurrentMonth = true
this.start = dayjs().startOf('month').unix()
this.date = dayjs.tz().format('YYYY-MM-DD')
} else {
this.isCurrentMonth = false
this.date = ''
isCurrentMonth = false
this.start = dayjs().year(year).month(month - 1).startOf('month').unix() // .startOf('week').unix()
}
this.end = dayjs().year(year).month(month).endOf('month').unix() // .endOf('week').unix()
await this.updateEvents()
await this.$fetch()
this.$nuxt.$loading.finish()
})
this.$nextTick( () => this.isCurrentMonth = isCurrentMonth)
},
dayChange (day) {

View file

@ -32,7 +32,7 @@ export default {
async asyncData ({ $axios, params, error }) {
try {
const tag = params.tag
const events = await $axios.$get(`/tag/${tag}`)
const events = await $axios.$get(`/tag/${encodeURIComponent(tag)}`)
return { events, tag }
} catch (e) {
error({ statusCode: 400, message: 'Error!' })

View file

@ -18,12 +18,15 @@ export default ({ $axios }, inject) => {
try {
const events = await $axios.$get('/events', {
params: {
start: params.start,
end: params.end,
...params,
// start: params.start,
// end: params.end,
places: params.places && params.places.join(','),
tags: params.tags && params.tags.join(','),
show_recurrent: !!params.show_recurrent,
max: params.maxs
// ...(params.show_recurrent !== && {show_recurrent: !!params.show_recurrent}),
// show_multidate: !!params.show_multidate,
// query: params.query,
// max: params.maxs
}
})
return events.map(e => Object.freeze(e))

View file

@ -16,6 +16,7 @@ import 'dayjs/locale/fr'
import 'dayjs/locale/de'
import 'dayjs/locale/gl'
import 'dayjs/locale/sk'
import 'dayjs/locale/ru'
import 'dayjs/locale/pt'
import 'dayjs/locale/zh'
@ -57,11 +58,11 @@ export default ({ app, store }) => {
return ''
})
Vue.filter('from', timestamp => dayjs.unix(timestamp).tz().fromNow())
Vue.filter('from', timestamp => dayjs.unix(timestamp).tz().locale(app.i18n.locale || store.state.settings.instance_locale).fromNow())
Vue.filter('recurrentDetail', event => {
const parent = event.parent
if (!parent.recurrent || parent.recurrent.frequency) return 'error!'
if (!parent.recurrent || !parent.recurrent.frequency) return 'error!'
const { frequency, type } = parent.recurrent
let recurrent
if (frequency === '1w' || frequency === '2w') {

View file

@ -4,7 +4,7 @@ rm -fr node_modules
yarn
yarn build
yarn pack
# yarn publish
yarn publish
gpg --pinentry-mode loopback --passphrase `pass underscore/pgp` --detach-sign --local-user 5DAC477D5441B7A15ACBF680BBEB4DD39AC6CCA9 gancio-$RELEASE.tgz
cp gancio-$RELEASE.tgz releases/
mv gancio-$RELEASE.tgz releases/latest.tgz

View file

@ -1,4 +1,5 @@
const Announcement = require('../models/announcement')
const { Announcement } = require('../models/models')
const log = require('../../log')
const announceController = {

View file

@ -1,4 +1,4 @@
const APUser = require('../models/ap_user')
const { APUser } = require('../models/models')
const apUserController = {
async toggleBlock (req, res) {

View file

@ -1,8 +1,5 @@
const Collection = require('../models/collection')
const Filter = require('../models/filter')
const Event = require('../models/event')
const Tag = require('../models/tag')
const Place = require('../models/place')
const { Collection, Filter, Event, Tag, Place } = require('../models/models')
const log = require('../../log')
const dayjs = require('dayjs')
const { col: Col } = require('../../helpers')
@ -114,7 +111,7 @@ const collectionController = {
res.json(collection)
} catch (e) {
log.error(`Create collection failed ${e}`)
res.sendStatus(400)
res.status(400).send(e)
}
},
@ -138,15 +135,14 @@ const collectionController = {
},
async addFilter (req, res) {
const collectionId = req.body.collectionId
const tags = req.body.tags
const places = req.body.places
const { collectionId, tags, places } = req.body
try {
const filter = await Filter.create({ collectionId, tags, places })
filter = await Filter.create({ collectionId, tags, places })
return res.json(filter)
} catch (e) {
log.error(String(e))
return res.status(500)
return res.sendStatus(400)
}
},
@ -170,6 +166,4 @@ const collectionController = {
}
module.exports = collectionController

View file

@ -3,18 +3,15 @@ const path = require('path')
const config = require('../../config')
const fs = require('fs')
const { Op } = require('sequelize')
const intersection = require('lodash/intersection')
const linkifyHtml = require('linkify-html')
const Sequelize = require('sequelize')
const dayjs = require('dayjs')
const helpers = require('../../helpers')
const Col = helpers.col
const Event = require('../models/event')
const Resource = require('../models/resource')
const Tag = require('../models/tag')
const Place = require('../models/place')
const Notification = require('../models/notification')
const APUser = require('../models/ap_user')
const notifier = require('../../notifier')
const { Event, Resource, Tag, Place, Notification, APUser } = require('../models/models')
const exportController = require('./export')
const tagController = require('./tag')
@ -89,99 +86,71 @@ const eventController = {
},
async search(req, res) {
const search = req.query.search.trim().toLocaleLowerCase()
const show_recurrent = req.query.show_recurrent || false
const end = req.query.end
const replacements = []
// async search(req, res) {
// const search = req.query.search.trim().toLocaleLowerCase()
// const show_recurrent = req.query.show_recurrent || false
// const end = req.query.end
// const replacements = []
const where = {
// do not include parent recurrent event
recurrent: null,
// const where = {
// // do not include parent recurrent event
// recurrent: null,
// confirmed event only
is_visible: true,
// // confirmed event only
// is_visible: true,
}
// }
if (!show_recurrent) {
where.parentId = null
}
// if (!show_recurrent) {
// where.parentId = null
// }
if (end) {
where.start_datetime = { [Op.lte]: end }
}
// if (end) {
// where.start_datetime = { [Op.lte]: end }
// }
if (search) {
replacements.push(search)
where[Op.or] =
[
{ title: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('title')), 'LIKE', '%' + search + '%') },
Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + search + '%'),
Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) = ?`))
]
}
// if (search) {
// replacements.push(search)
// where[Op.or] =
// [
// { title: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('title')), 'LIKE', '%' + search + '%') },
// Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + search + '%'),
// Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) = ?`))
// ]
// }
const events = await Event.findAll({
where,
attributes: {
exclude: ['likes', 'boost', 'userId', 'is_visible', 'createdAt', 'updatedAt', 'description', 'resources']
},
order: [['start_datetime', 'DESC']],
include: [
{
model: Tag,
// order: [Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')],
attributes: ['tag'],
through: { attributes: [] }
},
{ model: Place, required: true, attributes: ['id', 'name', 'address', 'latitude', 'longitude'] }
],
replacements,
limit: 30,
}).catch(e => {
log.error('[EVENT]', e)
return res.json([])
})
// const events = await Event.findAll({
// where,
// attributes: {
// exclude: ['likes', 'boost', 'userId', 'is_visible', 'createdAt', 'updatedAt', 'description', 'resources']
// },
// order: [['start_datetime', 'DESC']],
// include: [
// {
// model: Tag,
// // order: [Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')],
// attributes: ['tag'],
// through: { attributes: [] }
// },
// { model: Place, required: true, attributes: ['id', 'name', 'address', 'latitude', 'longitude'] }
// ],
// replacements,
// limit: 30,
// }).catch(e => {
// log.error('[EVENT]', e)
// return res.json([])
// })
const ret = events.map(e => {
e = e.get()
e.tags = e.tags ? e.tags.map(t => t && t.tag) : []
return e
})
// const ret = events.map(e => {
// e = e.get()
// e.tags = e.tags ? e.tags.map(t => t && t.tag) : []
// return e
// })
return res.json(ret)
// return res.json(ret)
},
async getNotifications(event, action) {
log.debug(`getNotifications ${event.title} ${action}`)
function match(event, filters) {
// matches if no filter specified
if (!filters) { return true }
// check for visibility
if (typeof filters.is_visible !== 'undefined' && filters.is_visible !== event.is_visible) { return false }
if (!filters.tags && !filters.places) { return true }
if (!filters.tags.length && !filters.places.length) { return true }
if (filters.tags.length) {
const m = intersection(event.tags.map(t => t.tag), filters.tags)
if (m.length > 0) { return true }
}
if (filters.places.length) {
if (filters.places.find(p => p === event.place.name)) {
return true
}
}
}
const notifications = await Notification.findAll({ where: { action }, include: [Event] })
// get notification that matches with selected event
return notifications.filter(notification => match(event, notification.filters))
},
// },
async _get(slug) {
// retrocompatibility, old events URL does not use slug, use id as fallback
@ -317,7 +286,6 @@ const eventController = {
res.sendStatus(200)
// send notification
const notifier = require('../../notifier')
notifier.notifyEvent('Create', event.id)
} catch (e) {
log.error('[EVENT]', e)
@ -631,9 +599,11 @@ const eventController = {
async _select({
start = dayjs().unix(),
end,
query,
tags,
places,
show_recurrent,
show_multidate,
limit,
page,
older }) {
@ -656,6 +626,10 @@ const eventController = {
where.parentId = null
}
if (!show_multidate) {
where.multidate = { [Op.not]: true }
}
if (end) {
where.start_datetime = { [older ? Op.gte : Op.lte]: end }
}
@ -679,6 +653,16 @@ const eventController = {
where.placeId = places.split(',')
}
if (query) {
replacements.push(query)
where[Op.or] =
[
{ title: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('title')), 'LIKE', '%' + query + '%') },
Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + query + '%'),
Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) = ?`))
]
}
let pagination = {}
if (limit) {
pagination = {
@ -705,7 +689,7 @@ const eventController = {
...pagination,
replacements
}).catch(e => {
log.error('[EVENT]', e)
log.error('[EVENT]' + String(e))
return []
})
@ -723,17 +707,21 @@ const eventController = {
const settings = res.locals.settings
const start = req.query.start || dayjs().unix()
const end = req.query.end
const query = req.query.query
const tags = req.query.tags
const places = req.query.places
const limit = Number(req.query.max) || 0
const page = Number(req.query.page) || 0
const older = req.query.older || false
const show_multidate = settings.allow_multidate_event &&
typeof req.query.show_multidate !== 'undefined' ? req.query.show_multidate !== 'false' : true
const show_recurrent = settings.allow_recurrent_event &&
typeof req.query.show_recurrent !== 'undefined' ? req.query.show_recurrent === 'true' : settings.recurrent_event_visible
res.json(await eventController._select({
start, end, places, tags, show_recurrent, limit, page, older
start, end, query, places, tags, show_recurrent, show_multidate, limit, page, older
}))
},

View file

@ -1,6 +1,4 @@
const Event = require('../models/event')
const Place = require('../models/place')
const Tag = require('../models/tag')
const { Event, Place, Tag } = require('../models/models')
const { htmlToText } = require('html-to-text')
const { Op, literal } = require('sequelize')
@ -88,6 +86,7 @@ const exportController = {
const start = tmpStart.utc(true).format('YYYY-M-D-H-m').split('-').map(Number)
const end = tmpEnd.utc(true).format('YYYY-M-D-H-m').split('-').map(Number)
return {
uid: `${e.id}@${settings.hostname}`,
start,
end,
title: `[${settings.title}] ${e.title}`,

View file

@ -0,0 +1,122 @@
const log = require('../../log')
const nominatim = require('../../services/geocoding/nominatim')
const photon = require('../../services/geocoding/photon')
const axios = require('axios')
const { version } = require('../../../package.json')
let d = 0 // departure time
let h = 0 // hit geocoding provider time (aka Latency)
const geocodingController = {
/**
* Limit provider api usage.
* From https://operations.osmfoundation.org/policies/nominatim/
* [Requirements] No heavy uses (an absolute maximum of 1 request per second).
* [Websites and Apps]
* - Note that the usage limits above apply per website/application: the sum of traffic by all your users should not exceed the limits.
* - If at all possible, set up a proxy and also enable caching of requests.
*/
providerRateLimit (req, res, next, providerCache) {
let a = Date.now(); // arrival time
let dprev = d
d = dprev + 1000 + h
// console.log('a: ' + a)
// console.log('dprev: ' + dprev)
// console.log('d: ' + d)
// if the same request was already cached skip the delay mechanism
if (providerCache.get(req.params.place_details)) {
if (a < d) {
log.warn('More than 1 request per second to geocoding api. This from ' + req.ip + ' . The response data is served from memory-cache.')
}
// reset departure time because there is no need to ask provider
d = dprev
return next()
}
if (d === 0 || a > d) {
// no-queue or old-queue
// console.log('No queue or Old queue')
// arrival time + 10ms estimated computing time
d = a + 10
next()
} else {
// fresh queue
// console.log('Fresh queue')
let wait = d - a
// console.log('Waiting '+ wait)
log.warn('More than 1 request per second to geocoding api. This from ' + req.ip + ' . Applying ToS padding before asking to provider. The response data is now cached.')
setTimeout(() => {
next()
}, wait)
}
},
async nominatimRateLimit(req, res, next) {
geocodingController.providerRateLimit(req, res, next, nominatim.cache)
},
async photonRateLimit(req, res, next) {
geocodingController.providerRateLimit(req, res, next, photon.cache)
},
async checkInCache (req, res, details, providerCache) {
const ret = await providerCache.get(details)
if (ret) {
return ret
} else {
return
}
},
async queryProvider(req, res, details, provider) {
let RTTstart = Date.now()
// console.log('Asking Provider: ' + RTTstart)
const ret = await axios.get(`${provider.endpoint(req, res)}`, {
params: provider.getParams(req, res),
headers: { 'User-Agent': `gancio ${version}` }
})
if (ret) {
let RTTend = Date.now()
// console.log('Asking Provider: ' + RTTend)
// Save the hit time (aka Latency)
// console.log('Saving latency h: ' + h)
h = (RTTend - RTTstart) / 2
}
// Cache the response data
provider.cache.put(details, ret.data, 1000 * 60 * 60 * 24);
// console.log(cache.keys())
// console.log(cache.exportJson())
return ret.data
},
async _nominatim (req, res) {
const details = req.params.place_details
const ret = await geocodingController.checkInCache(req, res, details, nominatim.cache) ||
await geocodingController.queryProvider(req, res, details, nominatim)
return res.json(ret)
},
async _photon (req, res) {
const details = req.params.place_details
const ret = await geocodingController.checkInCache(req, res, details, photon.cache) ||
await geocodingController.queryProvider(req, res, details, photon)
return res.json(ret)
},
}
module.exports = geocodingController

View file

@ -1,6 +1,5 @@
const APUser = require('../models/ap_user')
const Instance = require('../models/instance')
const Resource = require('../models/resource')
const { APUser, Instance, Resource } = require('../models/models')
const Sequelize = require('sequelize')
const instancesController = {

View file

@ -1,4 +1,4 @@
const User = require('../models/user')
const User = require('../models/modles')
const metrics = {

View file

@ -2,12 +2,9 @@ const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const session = require('cookie-session')
const OAuthClient = require('../models/oauth_client')
const OAuthToken = require('../models/oauth_token')
const OAuthCode = require('../models/oauth_code')
const { OAuthClient, OAuthToken, OAuthCode, User } = require('../models/models')
const helpers = require('../../helpers.js')
const User = require('../models/user')
const passport = require('passport')
const get = require('lodash/get')

View file

@ -1,12 +1,10 @@
const Place = require('../models/place')
const Event = require('../models/event')
const { Place, Event } = require('../models/models')
const eventController = require('./event')
const exportController = require('./export')
const log = require('../../log')
const { Op, where, col, fn, cast } = require('sequelize')
const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search'
const axios = require('axios')
module.exports = {
@ -72,23 +70,6 @@ module.exports = {
// TOFIX: don't know why limit does not work
return res.json(places.slice(0, 10))
},
async _nominatim (req, res) {
const details = req.params.place_details
// ?limit=3&format=json&namedetails=1&addressdetails=1&q=
const ret = await axios.get(`${NOMINATIM_URL}`, {
params: {
q: details,
limit: 3,
format: 'json',
addressdetails: 1,
namedetails: 1
},
headers: { 'User-Agent': 'gancio 1.6.0' }
})
return res.json(ret.data)
},
}
}

View file

@ -2,11 +2,12 @@ const path = require('path')
const fs = require('fs')
const log = require('../../log')
const config = require('../../config')
const settingsController = require('./settings')
const notifier = require('../../notifier')
const pluginController = {
plugins: [],
getAll(_req, res) {
const settingsController = require('./settings')
// return plugins and inner settings
const plugins = pluginController.plugins.map( ({ configuration }) => {
if (settingsController.settings['plugin_' + configuration.name]) {
@ -18,7 +19,6 @@ const pluginController = {
},
togglePlugin(req, res) {
const settingsController = require('./settings')
const pluginName = req.params.plugin
const pluginSettings = settingsController.settings['plugin_' + pluginName]
if (!pluginSettings) { return res.sendStatus(404) }
@ -33,7 +33,6 @@ const pluginController = {
},
unloadPlugin(pluginName) {
const settingsController = require('./settings')
const plugin = pluginController.plugins.find(p => p.configuration.name === pluginName)
const settings = settingsController.settings['plugin_' + pluginName]
if (!plugin) {
@ -59,14 +58,12 @@ const pluginController = {
},
loadPlugin(pluginName) {
const settingsController = require('./settings')
const plugin = pluginController.plugins.find(p => p.configuration.name === pluginName)
const settings = settingsController.settings['plugin_' + pluginName]
if (!plugin) {
log.warn(`Plugin ${pluginName} not found`)
return
}
const notifier = require('../../notifier')
log.info('Load plugin ' + pluginName)
if (typeof plugin.onEventCreate === 'function') {
notifier.emitter.on('Create', plugin.onEventCreate)
@ -79,20 +76,15 @@ const pluginController = {
}
if (plugin.load && typeof plugin.load === 'function') {
plugin.load({ helpers: require('../../helpers'), settings: settingsController.settings }, settings)
plugin.load({
helpers: require('../../helpers'),
settings: settingsController.settings
},
settings)
}
},
_load() {
const settingsController = require('./settings')
// load custom plugins
const plugins_path = config.plugins_path || path.resolve(process.env.cwd || '', 'gancio_plugins')
log.info(`Loading plugin ${plugins_path}`)
if (fs.existsSync(plugins_path)) {
const plugins = fs.readdirSync(plugins_path)
.map(e => path.resolve(plugins_path, e, 'index.js'))
.filter(index => fs.existsSync(index))
plugins.forEach(pluginFile => {
_loadPlugin (pluginFile) {
try {
const plugin = require(pluginFile)
const name = plugin.configuration.name
@ -109,7 +101,22 @@ const pluginController = {
} catch (e) {
log.warn(`Unable to load plugin ${pluginFile}: ${String(e)}`)
}
})
},
_load() {
// load custom plugins
const system_plugins_path = path.resolve(__dirname || '', '../../../gancio_plugins')
const custom_plugins_path = config.plugins_path || path.resolve(process.env.cwd || '', 'plugins')
const plugins_paths = custom_plugins_path === system_plugins_path ? [custom_plugins_path] : [custom_plugins_path, system_plugins_path]
log.info(`Loading plugins from ${plugins_paths.join(' and ')}`)
for (const plugins_path of plugins_paths) {
if (fs.existsSync(plugins_path)) {
fs.readdirSync(plugins_path)
.map(e => path.resolve(plugins_path, e, 'index.js'))
.filter(index => fs.existsSync(index))
.forEach(pluginController._loadPlugin)
}
}
}
}

View file

@ -1,6 +1,4 @@
const Resource = require('../models/resource')
const APUser = require('../models/ap_user')
const Event = require('../models/event')
const { Resource, APUser, Event } = require('../models/models')
const get = require('lodash/get')
const resourceController = {

View file

@ -1,6 +1,5 @@
const path = require('path')
const URL = require('url')
const fs = require('fs')
const crypto = require('crypto')
const { promisify } = require('util')
const sharp = require('sharp')
@ -9,7 +8,7 @@ const generateKeyPair = promisify(crypto.generateKeyPair)
const log = require('../../log')
// const locales = require('../../../locales/index')
const escape = require('lodash/escape')
const pluginController = require('./plugins')
const DB = require('../models/models')
let defaultHostname
try {
@ -27,9 +26,15 @@ const defaultSettings = {
instance_place: '',
allow_registration: true,
allow_anon_event: true,
allow_multidate_event: true,
allow_recurrent_event: false,
recurrent_event_visible: false,
allow_geolocation: true,
allow_geolocation: false,
geocoding_provider_type: 'Nominatim',
geocoding_provider: 'https://nominatim.openstreetmap.org/search',
geocoding_countrycodes: [],
tilelayer_provider: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
tilelayer_provider_attribution: "<a target=\"_blank\" href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors",
enable_federation: true,
enable_resources: false,
hide_boosts: true,
@ -68,8 +73,7 @@ const settingsController = {
// initialize instance settings from db
// note that this is done only once when the server starts
// and not for each request
const Setting = require('../models/setting')
const settings = await Setting.findAll()
const settings = await DB.Setting.findAll()
settingsController.settings = defaultSettings
settings.forEach(s => {
if (s.is_secret) {
@ -111,15 +115,14 @@ const settingsController = {
// }
// })
// }
const pluginController = require('./plugins')
pluginController._load()
},
async set (key, value, is_secret = false) {
const Setting = require('../models/setting')
log.info(`SET ${key} ${is_secret ? '*****' : value}`)
try {
const [setting, created] = await Setting.findOrCreate({
const [setting, created] = await DB.Setting.findOrCreate({
where: { key },
defaults: { value, is_secret }
})
@ -211,7 +214,7 @@ const settingsController = {
}
const uploadedPath = path.join(req.file.destination, req.file.filename)
const baseImgPath = path.resolve(config.upload_path, 'fallbackImage.png')
const baseImgPath = path.resolve(config.upload_path, 'headerImage.png')
// convert and resize to png
return sharp(uploadedPath)

View file

@ -7,6 +7,8 @@ const settingsController = require('./settings')
const path = require('path')
const escape = require('lodash/escape')
const DB = require('../models/models')
const setupController = {
async _setupDb (dbConf) {
@ -23,7 +25,10 @@ const setupController = {
// try to connect
dbConf.logging = false
await db.connect(dbConf)
db.connect(dbConf)
db.loadModels()
db.associates()
await db.sequelize.authenticate()
// is empty ?
const isEmpty = await db.isEmpty()
@ -69,8 +74,7 @@ const setupController = {
// create admin
const password = helpers.randomString()
const email = `admin`
const User = require('../models/user')
await User.create({
await DB.User.create({
email,
password,
is_admin: true,

View file

@ -1,6 +1,7 @@
const Tag = require('../models/tag')
const Event = require('../models/event')
const { Tag, Event } = require('../models/models')
const uniq = require('lodash/uniq')
const log = require('../../log')
const { where, fn, col, Op } = require('sequelize')
const exportController = require('./export')
@ -45,6 +46,20 @@ module.exports = {
}
},
async getAll (_req, res) {
const tags = await Tag.findAll({
order: [[fn('COUNT', col('tag.tag')), 'DESC']],
attributes: ['tag', [fn('COUNT', col('tag.tag')), 'count']],
include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }],
group: ['tag.tag'],
raw: true,
})
return res.json(tags)
},
/**
* search for tags by query string
* sorted by usage
@ -60,9 +75,54 @@ module.exports = {
include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }],
group: ['tag.tag'],
limit: 10,
subQuery:false
subQuery: false
})
return res.json(tags.map(t => t.tag))
},
// async updateTag (req, res) {
// const tag = await Tag.findByPk(req.body.tag)
// await tag.update(req.body)
// res.json(place)
// },
async updateTag (req, res) {
try {
const oldtag = await Tag.findByPk(req.body.tag)
const newtag = await Tag.findByPk(req.body.newTag)
// if the new tag does not exists, just rename the old one
if (!newtag) {
log.info(`Rename tag ${oldtag.tag} to ${req.body.newTag}`)
await Tag.update({ tag: req.body.newTag }, { where: { tag: req.body.tag }, raw: true })
} else {
// in case it exists:
// - search for events with old tag
const events = await oldtag.getEvents()
// - substitute it with the new one
await oldtag.removeEvents(events)
await newtag.addEvents(events)
}
res.sendStatus(200)
} catch (e) {
console.error(e)
res.sendStatus(400)
}
},
async remove (req, res) {
log.info('Remove tag', req.params.tag)
const tagName = req.params.tag
try {
const tag = await Tag.findByPk(tagName)
await tag.destroy()
res.sendStatus(200)
} catch (e) {
log.error('Tag removal failed:', e)
res.sendStatus(404)
}
}
}

View file

@ -2,7 +2,7 @@ const crypto = require('crypto')
const { Op } = require('sequelize')
const config = require('../../config')
const mail = require('../mail')
const User = require('../models/user')
const { User } = require('../models/models')
const settingsController = require('./settings')
const log = require('../../log')
const linkify = require('linkifyjs')

View file

@ -5,38 +5,47 @@ const cors = require('cors')()
const config = require('../config')
const log = require('../log')
const api = express.Router()
api.use(express.urlencoded({ extended: false }))
api.use(express.json())
const collectionController = require('./controller/collection')
const setupController = require('./controller/setup')
const settingsController = require('./controller/settings')
const eventController = require('./controller/event')
const placeController = require('./controller/place')
const tagController = require('./controller/tag')
const exportController = require('./controller/export')
const userController = require('./controller/user')
const instanceController = require('./controller/instance')
const apUserController = require('./controller/ap_user')
const resourceController = require('./controller/resource')
const oauthController = require('./controller/oauth')
const announceController = require('./controller/announce')
const pluginController = require('./controller/plugins')
const geocodingController = require('./controller/geocoding')
const { DDOSProtectionApiRateLimiter, SPAMProtectionApiRateLimiter } = require('./limiter')
const helpers = require('../helpers')
const storage = require('./storage')
if (config.status !== 'READY') {
module.exports = () => {
const api = express.Router()
api.use(express.urlencoded({ extended: false }))
api.use(express.json())
if (process.env.NODE_ENV !== 'test') {
api.use(DDOSProtectionApiRateLimiter)
}
if (config.status !== 'READY') {
const setupController = require('./controller/setup')
const settingsController = require('./controller/settings')
api.post('/settings', settingsController.setRequest)
api.post('/setup/db', setupController.setupDb)
api.post('/setup/restart', setupController.restart)
api.post('/settings/smtp', settingsController.testSMTP)
} else {
} else {
const { isAuth, isAdmin } = require('./auth')
const eventController = require('./controller/event')
const placeController = require('./controller/place')
const tagController = require('./controller/tag')
const settingsController = require('./controller/settings')
const exportController = require('./controller/export')
const userController = require('./controller/user')
const instanceController = require('./controller/instance')
const apUserController = require('./controller/ap_user')
const resourceController = require('./controller/resource')
const oauthController = require('./controller/oauth')
const announceController = require('./controller/announce')
const collectionController = require('./controller/collection')
const pluginController = require('./controller/plugins')
const helpers = require('../helpers')
const storage = require('./storage')
const upload = multer({ storage })
/**
@ -62,13 +71,12 @@ if (config.status !== 'READY') {
api.get('/ping', (_req, res) => res.sendStatus(200))
api.get('/user', isAuth, (req, res) => res.json(req.user))
api.post('/user/recover', userController.forgotPassword)
api.post('/user/recover', SPAMProtectionApiRateLimiter, userController.forgotPassword)
api.post('/user/check_recover_code', userController.checkRecoverCode)
api.post('/user/recover_password', userController.updatePasswordWithRecoverCode)
api.post('/user/recover_password', SPAMProtectionApiRateLimiter, userController.updatePasswordWithRecoverCode)
// register and add users
api.post('/user/register', userController.register)
api.post('/user/register', SPAMProtectionApiRateLimiter, userController.register)
api.post('/user', isAdmin, userController.create)
// update user
@ -88,6 +96,7 @@ if (config.status !== 'READY') {
* @type GET
* @param {integer} [start] - start timestamp (default: now)
* @param {integer} [end] - end timestamp (optional)
* @param {string} [query] - search for this string
* @param {array} [tags] - List of tags
* @param {array} [places] - List of places id
* @param {integer} [max] - Limit events
@ -123,9 +132,9 @@ if (config.status !== 'READY') {
*/
// allow anyone to add an event (anon event has to be confirmed, TODO: flood protection)
api.post('/event', eventController.isAnonEventAllowed, upload.single('image'), eventController.add)
api.post('/event', eventController.isAnonEventAllowed, SPAMProtectionApiRateLimiter, upload.single('image'), eventController.add)
api.get('/event/search', eventController.search)
// api.get('/event/search', eventController.search)
api.put('/event', isAuth, upload.single('image'), eventController.update)
api.get('/event/import', isAuth, helpers.importURL)
@ -137,8 +146,8 @@ if (config.status !== 'READY') {
api.get('/event/meta', eventController.searchMeta)
// add event notification TODO
api.post('/event/notification', eventController.addNotification)
api.delete('/event/notification/:code', eventController.delNotification)
// api.post('/event/notification', eventController.addNotification)
// api.delete('/event/notification/:code', eventController.delNotification)
api.post('/settings', isAdmin, settingsController.setRequest)
api.get('/settings', isAdmin, settingsController.getAll)
@ -156,20 +165,29 @@ if (config.status !== 'READY') {
api.put('/event/unconfirm/:event_id', isAuth, eventController.unconfirm)
// get event
api.get('/event/:event_slug.:format?', cors, eventController.get)
api.get('/event/detail/:event_slug.:format?', cors, eventController.get)
// export events (rss/ics)
api.get('/export/:type', cors, exportController.export)
api.get('/place/all', isAdmin, placeController.getAll)
// - PLACES
api.get('/places', isAdmin, placeController.getAll)
api.get('/place/:placeName', cors, placeController.getEvents)
api.get('/place', cors, placeController.search)
api.get('/placeNominatim/:place_details', cors, placeController._nominatim)
api.put('/place', isAdmin, placeController.updatePlace)
// - GEOCODING
api.get('/placeOSM/Nominatim/:place_details', helpers.isGeocodingEnabled, geocodingController.nominatimRateLimit, geocodingController._nominatim)
api.get('/placeOSM/Photon/:place_details', helpers.isGeocodingEnabled, geocodingController.photonRateLimit, geocodingController._photon)
// - TAGS
api.get('/tags', isAdmin, tagController.getAll)
api.get('/tag', cors, tagController.search)
api.get('/tag/:tag', cors, tagController.getEvents)
api.delete('/tag/:tag', isAdmin, tagController.remove)
api.put('/tag', isAdmin, tagController.updateTag)
// - FEDIVERSE INSTANCES, MODERATION, RESOURCES
api.get('/instances', isAdmin, instanceController.getAll)
@ -202,15 +220,16 @@ if (config.status !== 'READY') {
// OAUTH
api.get('/clients', isAuth, oauthController.getClients)
api.get('/client/:client_id', isAuth, oauthController.getClient)
api.post('/client', oauthController.createClient)
}
api.post('/client', SPAMProtectionApiRateLimiter, oauthController.createClient)
}
api.use((_req, res) => res.sendStatus(404))
api.use((_req, res) => res.sendStatus(404))
// Handle 500
api.use((error, _req, res, _next) => {
// Handle 500
api.use((error, _req, res, _next) => {
log.error('[API ERROR]', error)
res.status(500).send('500: Internal Server Error')
})
})
module.exports = api
return api
}

32
server/api/limiter.js Normal file
View file

@ -0,0 +1,32 @@
const rateLimit = require('express-rate-limit')
const log = require('../log')
const next = (req, res, next) => next()
const instanceApiRateLimiter = {
DDOSProtectionApiRateLimiter: (process.env.NODE_ENV === 'test' ? next : rateLimit({
windowMs: 60 * 1000, // 5 minutes
max: 100, // Limit each IP to 100 requests per `window` (here, per 5 minutes)
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
handler: (request, response, next, options) => {
log.warn(`DDOS protection api rate limiter: > 100req/minute/ip ${request.ip}`)
return response.status(options.statusCode).send(options.message)
}
})),
SPAMProtectionApiRateLimiter: (process.env.NODE_ENV === 'test' ? next : rateLimit({
windowMs: 5 * 60 * 1000, // 10 minutes
max: 3, // Limit each IP to 3 requests per `window` (here, per 15 minutes)
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
handler: (request, response, next, options) => {
log.warn(`SPAM protection api rate limiter: 3req/5min/ip ${request.ip}`)
return response.status(options.statusCode).send(options.message)
}
}))
}
module.exports = instanceApiRateLimiter

View file

@ -1,12 +1,6 @@
const sequelize = require('./index').sequelize
const { Model, DataTypes } = require('sequelize')
class Announcement extends Model {}
Announcement.init({
module.exports = (sequelize, DataTypes) =>
sequelize.define('announcement', {
title: DataTypes.STRING,
announcement: DataTypes.STRING,
visible: DataTypes.BOOLEAN
}, { sequelize, modelName: 'announcement' })
module.exports = Announcement
})

View file

@ -1,9 +1,6 @@
const sequelize = require('./index').sequelize
const { Model, DataTypes } = require('sequelize')
class APUser extends Model {}
APUser.init({
module.exports = (sequelize, DataTypes) =>
sequelize.define('ap_user', {
ap_id: {
type: DataTypes.STRING,
primaryKey: true
@ -11,6 +8,4 @@ APUser.init({
follower: DataTypes.BOOLEAN,
blocked: DataTypes.BOOLEAN,
object: DataTypes.JSON
}, { sequelize, modelName: 'ap_user' })
module.exports = APUser
})

View file

@ -1,10 +1,5 @@
const { Model, DataTypes } = require('sequelize')
const sequelize = require('./index').sequelize
class Collection extends Model {}
// TODO: slugify!
Collection.init({
module.exports = (sequelize, DataTypes) =>
sequelize.define('collection', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
@ -22,7 +17,4 @@ Collection.init({
isTop: {
type: DataTypes.BOOLEAN
}
}, { sequelize, modelName: 'collection', timestamps: false })
module.exports = Collection
}, { timestamps: false })

View file

@ -1,18 +1,5 @@
const config = require('../../config')
const { htmlToText } = require('html-to-text')
const { Model, DataTypes } = require('sequelize')
const SequelizeSlugify = require('sequelize-slugify')
const sequelize = require('./index').sequelize
const Resource = require('./resource')
const Notification = require('./notification')
const EventNotification = require('./eventnotification')
const Place = require('./place')
const User = require('./user')
const Tag = require('./tag')
const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc')
@ -20,9 +7,9 @@ const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
dayjs.extend(timezone)
class Event extends Model {}
Event.init({
// class Event extends Model {}
module.exports = (sequelize, DataTypes) => {
const Event = sequelize.define('event', {
id: {
allowNull: false,
type: DataTypes.INTEGER,
@ -51,29 +38,9 @@ Event.init({
recurrent: DataTypes.JSON,
likes: { type: DataTypes.JSON, defaultValue: [] },
boost: { type: DataTypes.JSON, defaultValue: [] }
}, { sequelize, modelName: 'event' })
})
Event.belongsTo(Place)
Place.hasMany(Event)
Event.belongsTo(User)
User.hasMany(Event)
Event.belongsToMany(Tag, { through: 'event_tags' })
Tag.belongsToMany(Event, { through: 'event_tags' })
Event.belongsToMany(Notification, { through: EventNotification })
Notification.belongsToMany(Event, { through: EventNotification })
Event.hasMany(Resource)
Resource.belongsTo(Event)
Event.hasMany(Event, { as: 'child', foreignKey: 'parentId' })
Event.belongsTo(Event, { as: 'parent' })
SequelizeSlugify.slugifyModel(Event, { source: ['title'], overwrite: false })
Event.prototype.toAP = function (username, locale, to = []) {
Event.prototype.toAP = function (username, locale, to = []) {
const tags = this.tags && this.tags.map(t => t.tag.replace(/[ #]/g, '_'))
const plainDescription = htmlToText(this.description && this.description.replace('\n', '').slice(0, 1000))
const content = `
@ -122,6 +89,6 @@ Event.prototype.toAP = function (username, locale, to = []) {
content,
summary: content
}
}
return Event
}
module.exports = Event

View file

@ -1,15 +1,9 @@
const sequelize = require('./index').sequelize
const { Model, DataTypes } = require('sequelize')
class EventNotification extends Model {}
EventNotification.init({
module.exports = (sequelize, DataTypes) =>
sequelize.define('event_notification', {
status: {
type: DataTypes.ENUM,
values: ['new', 'sent', 'error', 'sending'],
defaultValue: 'new',
index: true
}
}, { sequelize, modelName: 'event_notification' })
module.exports = EventNotification
})

View file

@ -1,10 +1,6 @@
const { Model, DataTypes } = require('sequelize')
const Collection = require('./collection')
const sequelize = require('./index').sequelize
class Filter extends Model {}
Filter.init({
module.exports = (sequelize, DataTypes) =>
sequelize.define('filter',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
@ -16,9 +12,9 @@ Filter.init({
places: {
type: DataTypes.JSON,
}
}, { sequelize, modelName: 'filter', timestamps: false })
Filter.belongsTo(Collection)
Collection.hasMany(Filter)
module.exports = Filter
}, {
indexes: [
{ fields: ['collectionId', 'tags', 'places'], unique: true }
],
timestamps: false
})

View file

@ -4,10 +4,77 @@ const Umzug = require('umzug')
const path = require('path')
const config = require('../../config')
const log = require('../../log')
const settingsController = require('../controller/settings')
const SequelizeSlugify = require('sequelize-slugify')
const DB = require('./models')
const models = {
Announcement: require('./announcement'),
APUser: require('./ap_user'),
Collection: require('./collection'),
Event: require('./event'),
EventNotification: require('./eventnotification'),
Filter: require('./filter'),
Instance: require('./instance'),
Notification: require('./notification'),
OAuthClient: require('./oauth_client'),
OAuthCode: require('./oauth_code'),
OAuthToken: require('./oauth_token'),
Place: require('./place'),
Resource: require('./resource'),
Setting: require('./setting'),
Tag: require('./tag'),
User: require('./user'),
}
const db = {
sequelize: null,
loadModels () {
for (const modelName in models) {
const m = models[modelName](db.sequelize, Sequelize.DataTypes)
DB[modelName] = m
}
},
associates () {
const { Filter, Collection, APUser, Instance, User, Event, EventNotification, Tag,
OAuthCode, OAuthClient, OAuthToken, Resource, Place, Notification } = DB
Filter.belongsTo(Collection)
Collection.hasMany(Filter)
Instance.hasMany(APUser)
APUser.belongsTo(Instance)
OAuthCode.belongsTo(User)
OAuthCode.belongsTo(OAuthClient, { as: 'client' })
OAuthToken.belongsTo(User)
OAuthToken.belongsTo(OAuthClient, { as: 'client' })
APUser.hasMany(Resource)
Resource.belongsTo(APUser)
Event.belongsTo(Place)
Place.hasMany(Event)
Event.belongsTo(User)
User.hasMany(Event)
Event.belongsToMany(Tag, { through: 'event_tags' })
Tag.belongsToMany(Event, { through: 'event_tags' })
Event.belongsToMany(Notification, { through: EventNotification })
Notification.belongsToMany(Event, { through: EventNotification })
Event.hasMany(Resource)
Resource.belongsTo(Event)
Event.hasMany(Event, { as: 'child', foreignKey: 'parentId' })
Event.belongsTo(Event, { as: 'parent' })
SequelizeSlugify.slugifyModel(Event, { source: ['title'], overwrite: false })
},
close() {
if (db.sequelize) {
return db.sequelize.close()
@ -28,7 +95,6 @@ const db = {
}
}
db.sequelize = new Sequelize(dbConf)
return db.sequelize.authenticate()
},
async isEmpty() {
try {
@ -57,13 +123,12 @@ const db = {
})
return umzug.up()
},
async initialize() {
initialize() {
if (config.status === 'CONFIGURED') {
try {
await db.connect()
log.debug('Running migrations')
await db.runMigrations()
return settingsController.load()
db.connect()
db.loadModels()
db.associates()
} catch (e) {
log.warn(` ⚠️ Cannot connect to db, check your configuration => ${e}`)
process.exit(1)

View file

@ -1,11 +1,5 @@
const sequelize = require('./index').sequelize
const { Model, DataTypes } = require('sequelize')
const APUser = require('./ap_user')
class Instance extends Model {}
Instance.init({
module.exports = (sequelize, DataTypes) =>
sequelize.define('instance', {
domain: {
primaryKey: true,
allowNull: false,
@ -14,9 +8,4 @@ Instance.init({
name: DataTypes.STRING,
blocked: DataTypes.BOOLEAN,
data: DataTypes.JSON
}, { sequelize, modelName: 'instance' })
Instance.hasMany(APUser)
APUser.belongsTo(Instance)
module.exports = Instance
})

View file

@ -0,0 +1,20 @@
// export default models
// Announcement: require('./announcement'),
// APUser: require('./ap_user'),
// Collection: require('./collection'),
// Event: require('./event'),
// EventNotification: require('./eventnotification'),
// Filter: require('./filter'),
// Instance: require('./instance'),
// Notification: require('./notification'),
// OAuthClient: require('./oauth_client'),
// OAuthCode: require('./oauth_code'),
// OAuthToken: require('./oauth_token'),
// Place: require('./place'),
// Resource: require('./resource'),
// Setting: require('./setting'),
// Tag: require('./tag'),
// User: require('./user'),
module.exports = {}

View file

@ -1,10 +1,5 @@
const sequelize = require('./index').sequelize
const { Model, DataTypes } = require('sequelize')
class Notification extends Model {}
Notification.init({
module.exports = (sequelize, DataTypes) =>
sequelize.define('notification', {
filters: DataTypes.JSON,
email: DataTypes.STRING,
remove_code: DataTypes.STRING,
@ -25,5 +20,3 @@ Notification.init({
fields: ['action', 'type']
}]
})
module.exports = Notification

View file

@ -1,10 +1,5 @@
const sequelize = require('./index').sequelize
const { Model, DataTypes } = require('sequelize')
class OAuthClient extends Model {}
OAuthClient.init({
module.exports = (sequelize, DataTypes) =>
sequelize.define('oauth_client', {
id: {
type: DataTypes.STRING,
primaryKey: true,
@ -15,6 +10,4 @@ OAuthClient.init({
scopes: DataTypes.STRING,
redirectUris: DataTypes.STRING,
website: DataTypes.STRING
}, { sequelize, modelName: 'oauth_client' })
module.exports = OAuthClient
})

View file

@ -1,13 +1,5 @@
const sequelize = require('./index').sequelize
const { Model, DataTypes } = require('sequelize')
const User = require('./user')
const OAuthClient = require('./oauth_client')
class OAuthCode extends Model {}
OAuthCode.init({
module.exports = (sequelize, DataTypes) =>
sequelize.define('oauth_code', {
authorizationCode: {
type: DataTypes.STRING,
primaryKey: true
@ -15,9 +7,4 @@ OAuthCode.init({
expiresAt: DataTypes.DATE,
scope: DataTypes.STRING,
redirect_uri: DataTypes.STRING
}, { sequelize, modelName: 'oauth_code' })
OAuthCode.belongsTo(User)
OAuthCode.belongsTo(OAuthClient, { as: 'client' })
module.exports = OAuthCode
})

View file

@ -1,13 +1,5 @@
const sequelize = require('./index').sequelize
const { Model, DataTypes } = require('sequelize')
const User = require('./user')
const OAuthClient = require('./oauth_client')
class OAuthToken extends Model {}
OAuthToken.init({
module.exports = (sequelize, DataTypes) =>
sequelize.define('oauth_token', {
accessToken: {
type: DataTypes.STRING,
allowNull: false,
@ -27,9 +19,4 @@ OAuthToken.init({
}
},
scope: DataTypes.STRING
}, { sequelize, modelName: 'oauth_token' })
OAuthToken.belongsTo(User)
OAuthToken.belongsTo(OAuthClient, { as: 'client' })
module.exports = OAuthToken
})

View file

@ -1,9 +1,5 @@
const { Model, DataTypes } = require('sequelize')
const sequelize = require('./index').sequelize
class Place extends Model {}
Place.init({
module.exports = (sequelize, DataTypes) =>
sequelize.define('place', {
name: {
type: DataTypes.STRING,
unique: true,
@ -13,6 +9,4 @@ Place.init({
address: DataTypes.STRING,
latitude: DataTypes.FLOAT,
longitude: DataTypes.FLOAT,
}, { sequelize, modelName: 'place' })
module.exports = Place
})

View file

@ -1,11 +1,5 @@
const { Model, DataTypes } = require('sequelize')
const sequelize = require('./index').sequelize
const APUser = require('./ap_user')
class Resource extends Model {}
Resource.init({
module.exports = (sequelize, DataTypes) =>
sequelize.define('resource', {
activitypub_id: {
type: DataTypes.STRING,
index: true,
@ -13,9 +7,4 @@ Resource.init({
},
hidden: DataTypes.BOOLEAN,
data: DataTypes.JSON
}, { sequelize, modelName: 'resource' })
APUser.hasMany(Resource)
Resource.belongsTo(APUser)
module.exports = Resource
})

View file

@ -1,9 +1,5 @@
const { Model, DataTypes } = require('sequelize')
const sequelize = require('./index').sequelize
class Setting extends Model {}
Setting.init({
module.exports = (sequelize, DataTypes) =>
sequelize.define('setting', {
key: {
type: DataTypes.STRING,
primaryKey: true,
@ -12,6 +8,4 @@ Setting.init({
},
value: DataTypes.JSON,
is_secret: DataTypes.BOOLEAN
}, { sequelize, modelName: 'setting' })
module.exports = Setting
})

View file

@ -1,15 +1,9 @@
const { Model, DataTypes } = require('sequelize')
const sequelize = require('./index').sequelize
class Tag extends Model {}
Tag.init({
module.exports = (sequelize, DataTypes) =>
sequelize.define('tag', {
tag: {
type: DataTypes.STRING,
allowNull: false,
index: true,
primaryKey: true
}
}, { sequelize, modelName: 'tag' })
module.exports = Tag
})

View file

@ -1,11 +1,8 @@
const bcrypt = require('bcryptjs')
const { Model, DataTypes } = require('sequelize')
const sequelize = require('./index').sequelize
class User extends Model {}
User.init({
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('user', {
settings: {
type: DataTypes.JSON,
defaultValue: []
@ -24,9 +21,7 @@ User.init({
recover_code: DataTypes.STRING,
is_admin: DataTypes.BOOLEAN,
is_active: DataTypes.BOOLEAN
}, {
sequelize,
modelName: 'user',
}, {
scopes: {
withoutPassword: {
attributes: { exclude: ['password', 'recover_code'] }
@ -35,19 +30,20 @@ User.init({
attributes: { exclude: ['password'] }
}
}
})
})
User.prototype.comparePassword = async function (pwd) {
User.prototype.comparePassword = async function (pwd) {
if (!this.password) { return false }
return bcrypt.compare(pwd, this.password)
}
}
User.beforeSave(async (user, _options) => {
User.beforeSave(async (user, _options) => {
if (user.changed('password')) {
const salt = await bcrypt.genSalt(10)
const hash = await bcrypt.hash(user.password, salt)
user.password = hash
}
})
})
module.exports = User
return User
}

View file

@ -9,7 +9,7 @@ function _initializeDB () {
async function modify (args) {
await _initializeDB()
const helpers = require('../helpers')
const User = require('../api/models/user')
const { User } = require('../api/models/models')
const user = await User.findOne({ where: { email: args.account } })
console.log()
if (!user) {
@ -33,7 +33,7 @@ async function modify (args) {
async function create (args) {
await _initializeDB()
const User = require('../api/models/user')
const { User } = require('../api/models/models')
const user = await User.create({
email: args.email,
is_active: true,
@ -46,7 +46,7 @@ async function create (args) {
async function remove (args) {
await _initializeDB()
const User = require('../api/models/user')
const { User } = require('../api/models/models')
const user = await User.findOne({
where: { email: args.email }
})
@ -58,7 +58,7 @@ async function remove (args) {
async function list () {
await _initializeDB()
const User = require('../api/models/user')
const { User } = require('../api/models/models')
const users = await User.findAll()
console.log()
users.forEach(u => console.log(`${u.id}\tadmin: ${u.is_admin}\tenabled: ${u.is_active}\temail: ${u.email}`))

View file

@ -1,4 +1,4 @@
const Event = require('../api/models/event')
const { Event } = require('../api/models/models')
const config = require('../config')
const log = require('../log')

View file

@ -1,10 +1,10 @@
const axios = require('axios')
// const request = require('request')
const crypto = require('crypto')
const config = require('../config')
const httpSignature = require('http-signature')
const APUser = require('../api/models/ap_user')
const Instance = require('../api/models/instance')
const { APUser, Instance } = require('../api/models/models')
const url = require('url')
const settingsController = require('../api/controller/settings')
const log = require('../log')

View file

@ -2,10 +2,8 @@ const express = require('express')
const router = express.Router()
const cors = require('cors')
const Users = require('./users')
const Event = require('../api/models/event')
const User = require('../api/models/user')
const Tag = require('../api/models/tag')
const Place = require('../api/models/place')
const { Event, User, Tag, Place } = require('../api/models/models')
const settingsController = require('../api/controller/settings')
const Helpers = require('./helpers')

View file

@ -1,32 +0,0 @@
const express = require('express')
const router = express.Router()
const cors = require('cors')
const settingsController = require('../api/controller/settings')
const config = require('../config')
const version = require('../../package.json').version
router.use(cors())
router.get('/', (req, res) => {
const ret = {
version: '1.0',
server: {
baseUrl: config.baseurl,
name: config.title,
software: 'Gancio',
version
},
protocols: ['activitypub'],
openRegistrations: settingsController.settings.allow_registration,
usage: {
users: {
total: 10
}
},
localPost: 3,
localComments: 0
}
res.json(ret)
})
module.exports = router

View file

@ -1,6 +1,4 @@
const Event = require('../api/models/event')
const Resource = require('../api/models/resource')
const APUser = require('../api/models/ap_user')
const { Event, Resource, APUser } = require('../api/models/models')
const log = require('../log')
const helpers = require('../helpers')

View file

@ -1,7 +1,5 @@
const Event = require('../api/models/event')
const Place = require('../api/models/place')
const APUser = require('../api/models/ap_user')
const Tag = require('../api/models/tag')
const { Event, Place, APUser, Tag } = require('../api/models/models')
const escape = require('lodash/escape')
const config = require('../config')
const log = require('../log')

View file

@ -1,8 +1,6 @@
const express = require('express')
const router = express.Router()
const Event = require('../api/models/event')
const Resource = require('../api/models/resource')
const User = require('../api/models/user')
const { Event, Resource, User } = require('../api/models/models')
const cors = require('cors')
const settingsController = require('../api/controller/settings')
@ -65,7 +63,9 @@ router.get('/nodeinfo/:nodeinfo_version', async (req, res) => {
metadata: {
nodeDescription: settings.description,
nodeName: settings.title,
nodeLabel: settings.instance_place
nodeLabel: settings.instance_place,
nodeTimezone: settings.instance_timezone,
nodeActor: settings.instance_name
},
openRegistrations: settings.allow_registration,
protocols: ['activitypub'],

View file

@ -1,6 +1,5 @@
const ical = require('ical.js')
const settingsController = require('./api/controller/settings')
const acceptLanguage = require('accept-language')
const express = require('express')
const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone')
@ -15,7 +14,6 @@ const axios = require('axios')
const crypto = require('crypto')
const Microformats = require('microformat-node')
const get = require('lodash/get')
const cloneDeep = require('lodash/cloneDeep')
const DOMPurify = require('dompurify')
const { JSDOM } = require('jsdom')
@ -80,6 +78,7 @@ module.exports = {
allow_registration: settings.allow_registration,
allow_anon_event: settings.allow_anon_event,
allow_recurrent_event: settings.allow_recurrent_event,
allow_multidate_event: settings.allow_multidate_event,
recurrent_event_visible: settings.recurrent_event_visible,
enable_federation: settings.enable_federation,
enable_resources: settings.enable_resources,
@ -92,6 +91,11 @@ module.exports = {
hide_thumbs: settings.hide_thumbs,
hide_calendar: settings.hide_calendar,
allow_geolocation: settings.allow_geolocation,
geocoding_provider_type: settings.geocoding_provider_type,
geocoding_provider: settings.geocoding_provider,
geocoding_countrycodes: settings.geocoding_countrycodes,
tilelayer_provider: settings.tilelayer_provider,
tilelayer_provider_attribution: settings.tilelayer_provider_attribution,
footerLinks: settings.footerLinks,
about: settings.about
}
@ -113,14 +117,14 @@ module.exports = {
log.warn(err)
} else {
res.status(404).send('Not found (but nice try 😊)')
// }
}
}})
}
})
})
router.use('/fallbackimage.png', (req, res, next) => {
const fallbackImagePath = settingsController.settings.fallback_image || './static/noimg.svg'
return express.static(fallbackImagePath, { maxAge: '1d' })(req, res, next)
return express.static(fallbackImagePath)(req, res, next)
})
router.use('/headerimage.png', (req, res, next) => {
@ -130,12 +134,12 @@ module.exports = {
router.use('/logo.png', (req, res, next) => {
const logoPath = settingsController.settings.logo || './static/gancio'
return express.static(logoPath + '.png', {maxAge: '1d'})(req, res, next)
return express.static(logoPath + '.png')(req, res, next)
})
router.use('/favicon.ico', (req, res, next) => {
const faviconPath = res.locals.settings.logo ? res.locals.settings.logo + '.png' : './assets/favicon.ico'
return express.static(faviconPath, {maxAge: '1d'})(req, res, next)
return express.static(faviconPath)(req, res, next)
})
return router
@ -264,9 +268,9 @@ module.exports = {
},
async APRedirect(req, res, next) {
const eventController = require('../server/api/controller/event')
const acceptJson = req.accepts('html', 'application/activity+json') === 'application/activity+json'
if (acceptJson) {
const eventController = require('../server/api/controller/event')
const event = await eventController._get(req.params.slug)
if (event) {
return res.redirect(`/federation/m/${event.id}`)
@ -281,5 +285,13 @@ module.exports = {
return res.redirect((accepted === 'application/rss+xml' ? '/feed/rss' : '/feed/ics') + req.path)
}
next()
},
async isGeocodingEnabled(req, res, next) {
if (res.locals.settings.allow_geolocation) {
next()
} else {
res.sendStatus(403)
}
}
}

View file

@ -0,0 +1,987 @@
// Iso conversions
var isoCountries = [
{
"code": "af",
"name": "Afghanistan"
},
{
"code": "ax",
"name": "Aland Islands"
},
{
"code": "al",
"name": "Albania"
},
{
"code": "dz",
"name": "Algeria"
},
{
"code": "as",
"name": "American Samoa"
},
{
"code": "ad",
"name": "Andorra"
},
{
"code": "ao",
"name": "Angola"
},
{
"code": "ai",
"name": "Anguilla"
},
{
"code": "aq",
"name": "Antarctica"
},
{
"code": "ag",
"name": "Antigua And Barbuda"
},
{
"code": "ar",
"name": "Argentina"
},
{
"code": "am",
"name": "Armenia"
},
{
"code": "aw",
"name": "Aruba"
},
{
"code": "au",
"name": "Australia"
},
{
"code": "at",
"name": "Austria"
},
{
"code": "az",
"name": "Azerbaijan"
},
{
"code": "bs",
"name": "Bahamas"
},
{
"code": "bh",
"name": "Bahrain"
},
{
"code": "bd",
"name": "Bangladesh"
},
{
"code": "bb",
"name": "Barbados"
},
{
"code": "by",
"name": "Belarus"
},
{
"code": "be",
"name": "Belgium"
},
{
"code": "bz",
"name": "Belize"
},
{
"code": "bj",
"name": "Benin"
},
{
"code": "bm",
"name": "Bermuda"
},
{
"code": "bt",
"name": "Bhutan"
},
{
"code": "bo",
"name": "Bolivia"
},
{
"code": "ba",
"name": "Bosnia And Herzegovina"
},
{
"code": "bw",
"name": "Botswana"
},
{
"code": "bv",
"name": "Bouvet Island"
},
{
"code": "br",
"name": "Brazil"
},
{
"code": "io",
"name": "British Indian Ocean Territory"
},
{
"code": "bn",
"name": "Brunei Darussalam"
},
{
"code": "bg",
"name": "Bulgaria"
},
{
"code": "bf",
"name": "Burkina Faso"
},
{
"code": "bi",
"name": "Burundi"
},
{
"code": "kh",
"name": "Cambodia"
},
{
"code": "cm",
"name": "Cameroon"
},
{
"code": "ca",
"name": "Canada"
},
{
"code": "cv",
"name": "Cape Verde"
},
{
"code": "ky",
"name": "Cayman Islands"
},
{
"code": "cf",
"name": "Central African Republic"
},
{
"code": "td",
"name": "Chad"
},
{
"code": "cl",
"name": "Chile"
},
{
"code": "cn",
"name": "China"
},
{
"code": "cx",
"name": "Christmas Island"
},
{
"code": "cc",
"name": "Cocos (Keeling) Islands"
},
{
"code": "co",
"name": "Colombia"
},
{
"code": "km",
"name": "Comoros"
},
{
"code": "cg",
"name": "Congo"
},
{
"code": "cd",
"name": "Congo, Democratic Republic"
},
{
"code": "ck",
"name": "Cook Islands"
},
{
"code": "cr",
"name": "Costa Rica"
},
{
"code": "ci",
"name": "Cote D'Ivoire"
},
{
"code": "hr",
"name": "Croatia"
},
{
"code": "cu",
"name": "Cuba"
},
{
"code": "cy",
"name": "Cyprus"
},
{
"code": "cz",
"name": "Czech Republic"
},
{
"code": "dk",
"name": "Denmark"
},
{
"code": "dj",
"name": "Djibouti"
},
{
"code": "dm",
"name": "Dominica"
},
{
"code": "do",
"name": "Dominican Republic"
},
{
"code": "ec",
"name": "Ecuador"
},
{
"code": "eg",
"name": "Egypt"
},
{
"code": "sv",
"name": "El Salvador"
},
{
"code": "gq",
"name": "Equatorial Guinea"
},
{
"code": "er",
"name": "Eritrea"
},
{
"code": "ee",
"name": "Estonia"
},
{
"code": "et",
"name": "Ethiopia"
},
{
"code": "fk",
"name": "Falkland Islands (Malvinas)"
},
{
"code": "fo",
"name": "Faroe Islands"
},
{
"code": "fj",
"name": "Fiji"
},
{
"code": "fi",
"name": "Finland"
},
{
"code": "fr",
"name": "France"
},
{
"code": "gf",
"name": "French Guiana"
},
{
"code": "pf",
"name": "French Polynesia"
},
{
"code": "tf",
"name": "French Southern Territories"
},
{
"code": "ga",
"name": "Gabon"
},
{
"code": "gm",
"name": "Gambia"
},
{
"code": "ge",
"name": "Georgia"
},
{
"code": "de",
"name": "Germany"
},
{
"code": "gh",
"name": "Ghana"
},
{
"code": "gi",
"name": "Gibraltar"
},
{
"code": "gr",
"name": "Greece"
},
{
"code": "gl",
"name": "Greenland"
},
{
"code": "gd",
"name": "Grenada"
},
{
"code": "gp",
"name": "Guadeloupe"
},
{
"code": "gu",
"name": "Guam"
},
{
"code": "gt",
"name": "Guatemala"
},
{
"code": "gg",
"name": "Guernsey"
},
{
"code": "gn",
"name": "Guinea"
},
{
"code": "gw",
"name": "Guinea-Bissau"
},
{
"code": "gy",
"name": "Guyana"
},
{
"code": "ht",
"name": "Haiti"
},
{
"code": "hm",
"name": "Heard Island & Mcdonald Islands"
},
{
"code": "va",
"name": "Holy See (Vatican City State)"
},
{
"code": "hn",
"name": "Honduras"
},
{
"code": "hk",
"name": "Hong Kong"
},
{
"code": "hu",
"name": "Hungary"
},
{
"code": "is",
"name": "Iceland"
},
{
"code": "in",
"name": "India"
},
{
"code": "id",
"name": "Indonesia"
},
{
"code": "ir",
"name": "Iran, Islamic Republic Of"
},
{
"code": "iq",
"name": "Iraq"
},
{
"code": "ie",
"name": "Ireland"
},
{
"code": "im",
"name": "Isle Of Man"
},
{
"code": "il",
"name": "Israel"
},
{
"code": "it",
"name": "Italy"
},
{
"code": "jm",
"name": "Jamaica"
},
{
"code": "jp",
"name": "Japan"
},
{
"code": "je",
"name": "Jersey"
},
{
"code": "jo",
"name": "Jordan"
},
{
"code": "kz",
"name": "Kazakhstan"
},
{
"code": "ke",
"name": "Kenya"
},
{
"code": "ki",
"name": "Kiribati"
},
{
"code": "kr",
"name": "Korea"
},
{
"code": "kw",
"name": "Kuwait"
},
{
"code": "kg",
"name": "Kyrgyzstan"
},
{
"code": "la",
"name": "Lao People's Democratic Republic"
},
{
"code": "lv",
"name": "Latvia"
},
{
"code": "lb",
"name": "Lebanon"
},
{
"code": "ls",
"name": "Lesotho"
},
{
"code": "lr",
"name": "Liberia"
},
{
"code": "ly",
"name": "Libyan Arab Jamahiriya"
},
{
"code": "li",
"name": "Liechtenstein"
},
{
"code": "lt",
"name": "Lithuania"
},
{
"code": "lu",
"name": "Luxembourg"
},
{
"code": "mo",
"name": "Macao"
},
{
"code": "mk",
"name": "Macedonia"
},
{
"code": "mg",
"name": "Madagascar"
},
{
"code": "mw",
"name": "Malawi"
},
{
"code": "my",
"name": "Malaysia"
},
{
"code": "mv",
"name": "Maldives"
},
{
"code": "ml",
"name": "Mali"
},
{
"code": "mt",
"name": "Malta"
},
{
"code": "mh",
"name": "Marshall Islands"
},
{
"code": "mq",
"name": "Martinique"
},
{
"code": "mr",
"name": "Mauritania"
},
{
"code": "mu",
"name": "Mauritius"
},
{
"code": "yt",
"name": "Mayotte"
},
{
"code": "mx",
"name": "Mexico"
},
{
"code": "fm",
"name": "Micronesia, Federated States Of"
},
{
"code": "md",
"name": "Moldova"
},
{
"code": "mc",
"name": "Monaco"
},
{
"code": "mn",
"name": "Mongolia"
},
{
"code": "me",
"name": "Montenegro"
},
{
"code": "ms",
"name": "Montserrat"
},
{
"code": "ma",
"name": "Morocco"
},
{
"code": "mz",
"name": "Mozambique"
},
{
"code": "mm",
"name": "Myanmar"
},
{
"code": "na",
"name": "Namibia"
},
{
"code": "nr",
"name": "Nauru"
},
{
"code": "np",
"name": "Nepal"
},
{
"code": "nl",
"name": "Netherlands"
},
{
"code": "an",
"name": "Netherlands Antilles"
},
{
"code": "nc",
"name": "New Caledonia"
},
{
"code": "nz",
"name": "New Zealand"
},
{
"code": "ni",
"name": "Nicaragua"
},
{
"code": "ne",
"name": "Niger"
},
{
"code": "ng",
"name": "Nigeria"
},
{
"code": "nu",
"name": "Niue"
},
{
"code": "nf",
"name": "Norfolk Island"
},
{
"code": "mp",
"name": "Northern Mariana Islands"
},
{
"code": "no",
"name": "Norway"
},
{
"code": "om",
"name": "Oman"
},
{
"code": "pk",
"name": "Pakistan"
},
{
"code": "pw",
"name": "Palau"
},
{
"code": "ps",
"name": "Palestinian Territory, Occupied"
},
{
"code": "pa",
"name": "Panama"
},
{
"code": "pg",
"name": "Papua New Guinea"
},
{
"code": "py",
"name": "Paraguay"
},
{
"code": "pe",
"name": "Peru"
},
{
"code": "ph",
"name": "Philippines"
},
{
"code": "pn",
"name": "Pitcairn"
},
{
"code": "pl",
"name": "Poland"
},
{
"code": "pt",
"name": "Portugal"
},
{
"code": "pr",
"name": "Puerto Rico"
},
{
"code": "qa",
"name": "Qatar"
},
{
"code": "re",
"name": "Reunion"
},
{
"code": "ro",
"name": "Romania"
},
{
"code": "ru",
"name": "Russian Federation"
},
{
"code": "rw",
"name": "Rwanda"
},
{
"code": "bl",
"name": "Saint Barthelemy"
},
{
"code": "sh",
"name": "Saint Helena"
},
{
"code": "kn",
"name": "Saint Kitts And Nevis"
},
{
"code": "lc",
"name": "Saint Lucia"
},
{
"code": "mf",
"name": "Saint Martin"
},
{
"code": "pm",
"name": "Saint Pierre And Miquelon"
},
{
"code": "vc",
"name": "Saint Vincent And Grenadines"
},
{
"code": "ws",
"name": "Samoa"
},
{
"code": "sm",
"name": "San Marino"
},
{
"code": "st",
"name": "Sao Tome And Principe"
},
{
"code": "sa",
"name": "Saudi Arabia"
},
{
"code": "sn",
"name": "Senegal"
},
{
"code": "rs",
"name": "Serbia"
},
{
"code": "sc",
"name": "Seychelles"
},
{
"code": "sl",
"name": "Sierra Leone"
},
{
"code": "sg",
"name": "Singapore"
},
{
"code": "sk",
"name": "Slovakia"
},
{
"code": "si",
"name": "Slovenia"
},
{
"code": "sb",
"name": "Solomon Islands"
},
{
"code": "so",
"name": "Somalia"
},
{
"code": "za",
"name": "South Africa"
},
{
"code": "gs",
"name": "South Georgia And Sandwich Isl."
},
{
"code": "es",
"name": "Spain"
},
{
"code": "lk",
"name": "Sri Lanka"
},
{
"code": "sd",
"name": "Sudan"
},
{
"code": "sr",
"name": "Suriname"
},
{
"code": "sj",
"name": "Svalbard And Jan Mayen"
},
{
"code": "sz",
"name": "Swaziland"
},
{
"code": "se",
"name": "Sweden"
},
{
"code": "ch",
"name": "Switzerland"
},
{
"code": "sy",
"name": "Syrian Arab Republic"
},
{
"code": "tw",
"name": "Taiwan"
},
{
"code": "tj",
"name": "Tajikistan"
},
{
"code": "tz",
"name": "Tanzania"
},
{
"code": "th",
"name": "Thailand"
},
{
"code": "tl",
"name": "Timor-Leste"
},
{
"code": "tg",
"name": "Togo"
},
{
"code": "tk",
"name": "Tokelau"
},
{
"code": "to",
"name": "Tonga"
},
{
"code": "tt",
"name": "Trinidad And Tobago"
},
{
"code": "tn",
"name": "Tunisia"
},
{
"code": "tr",
"name": "Turkey"
},
{
"code": "tm",
"name": "Turkmenistan"
},
{
"code": "tc",
"name": "Turks And Caicos Islands"
},
{
"code": "tv",
"name": "Tuvalu"
},
{
"code": "ug",
"name": "Uganda"
},
{
"code": "ua",
"name": "Ukraine"
},
{
"code": "ae",
"name": "United Arab Emirates"
},
{
"code": "gb",
"name": "United Kingdom"
},
{
"code": "us",
"name": "United States"
},
{
"code": "um",
"name": "United States Outlying Islands"
},
{
"code": "uy",
"name": "Uruguay"
},
{
"code": "uz",
"name": "Uzbekistan"
},
{
"code": "vu",
"name": "Vanuatu"
},
{
"code": "ve",
"name": "Venezuela"
},
{
"code": "vn",
"name": "Viet Nam"
},
{
"code": "vg",
"name": "Virgin Islands, British"
},
{
"code": "vi",
"name": "Virgin Islands, U.S."
},
{
"code": "wf",
"name": "Wallis And Futuna"
},
{
"code": "eh",
"name": "Western Sahara"
},
{
"code": "ye",
"name": "Yemen"
},
{
"code": "zm",
"name": "Zambia"
},
{
"code": "zw",
"name": "Zimbabwe"
}
]
module.exports = { isoCountries }

View file

@ -1,5 +1,4 @@
const Place = require('../api/models/place')
const Event = require('../api/models/event')
const { Event, Place } = require('../api/models/models')
const Sequelize = require('sequelize')
const log = require('../log')

View file

@ -1,5 +1,5 @@
const Tag = require('../api/models/tag')
const Event = require('../api/models/event')
const { Event, Tag } = require('../api/models/models')
const Sequelize = require('sequelize')
const log = require('../log')
@ -13,7 +13,7 @@ module.exports = {
})
if (!tags.length) { return }
log.info(`Remove ${tags.length} unrelated tags`)
log.info(`Remove ${tags.length} orphan tags (${tags.join(', ')})`)
await Tag.destroy({
where: { tag: { [Sequelize.Op.in]: tags.map(p => p.tag) } }

View file

@ -1,4 +1,11 @@
const config = require('../server/config')
const db = require('./api/models/index')
const log = require('../server/log')
db.initialize()
const settingsController = require('./api/controller/settings')
const initialize = {
// close connections/port/unix socket
@ -19,14 +26,14 @@ const initialize = {
},
async start () {
const log = require('../server/log')
const settingsController = require('./api/controller/settings')
const db = require('./api/models/index')
const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone')
dayjs.extend(timezone)
if (config.status == 'CONFIGURED') {
await db.initialize()
await db.sequelize.authenticate()
log.debug('Running migrations')
await db.runMigrations()
await settingsController.load()
config.status = 'READY'
} else {
if (process.env.GANCIO_DB_DIALECT) {

Some files were not shown because too many files have changed in this diff Show more