Commit 3db1fee1 authored by zauberstuhl's avatar zauberstuhl
Browse files

Update frontend

parent ee93aa6c
......@@ -50,6 +50,7 @@ type Application struct {
Scheme string
Sessions *web.Sessions
OAuth *oauth2.Server
Framework app.Framework
}
// GetInboxWebHandlerFunc returns a function rendering the outbox. The framework
......@@ -359,7 +360,7 @@ func (a Application) GetUserWebHandlerFunc(app.Framework) (app.VocabHandlerFunc,
// process and serve ActivityStreams data, but the processing of the
// ActivityPub data itself is handled elsewhere in
// ApplyFederatingCallbacks and/or ApplySocialCallbacks.
func (a Application) BuildRoutes(r app.Router, db app.Database, f app.Framework) error {
func (a *Application) BuildRoutes(r app.Router, db app.Database, f app.Framework) error {
// When building routes, the framework already provides actors at the
// endpoint:
//
......@@ -385,6 +386,9 @@ func (a Application) BuildRoutes(r app.Router, db app.Database, f app.Framework)
//
// And supports using Webfinger to find actors on this server.
// Register framework within the application
a.Framework = f
// AP unrelated routes
r.NewRoute().Path(
fmt.Sprintf("%sapi/%s/config", a.Config.UnmappdConfig.SubFolder, apiVersion),
......
......@@ -23,7 +23,9 @@ import (
"strings"
"strconv"
"net/url"
"time"
"github.com/mdlayher/untappd"
"github.com/go-fed/activity/pub"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
......@@ -40,6 +42,10 @@ func apBuildNote(params url.Values) (vocab.ActivityStreamsNote, error) {
_, public := params["public"]
note := streams.NewActivityStreamsNote()
publishedProp := streams.NewActivityStreamsPublishedProperty()
publishedProp.Set(time.Now())
note.SetActivityStreamsPublished(publishedProp)
toProp := streams.NewActivityStreamsToProperty()
// skip if we haven't explicitly named an endpoint
if len(toParams) > 0 {
......@@ -72,6 +78,107 @@ func apBuildNote(params url.Values) (vocab.ActivityStreamsNote, error) {
return note, nil
}
func convertUntappdCheckin(checkin *untappd.Checkin) (vocab.SocialtapCheckin, error) {
if checkin.Beer == nil {
return nil, fmt.Errorf("checkin.Beer is mandatory")
}
if checkin.Brewery == nil {
return nil, fmt.Errorf("checkin.Brewery is mandatory")
}
params := make(url.Values)
// when was the checkin created/published
params["published"] = []string{checkin.Created.Format(time.RFC3339)}
// checkin
//
// checkin_media=https://untappd.akamaized.net/photos/2021_03_23/15ce52d1e5af86c3eb5c5ff7f66cf931_1280x1280.jpg
// checkin_rating=4.5
// checkin_comment=Awesome stuff!
//
for _, media := range checkin.Media {
if _, ok := params["checkin_media"]; ok {
params["checkin_media"] = append(params["checkin_media"], media.Photo.Og.String())
} else {
params["checkin_media"] = []string{media.Photo.Og.String()}
}
}
params["checkin_rating"] = []string{fmt.Sprintf("%.4f", checkin.UserRating)}
params["checkin_comment"] = []string{checkin.Comment}
// beer/drink
//
// drink_name=Tepache Sour
// drink_media=https://untappd.akamaized.net/site/beer_logos/beer-4138042_6540b_sm.jpeg
// drink_style=Sour - Fruited
// drink_category=Beer
// drink_abv=5
// drink_active=1
//
params["drink_category"] = []string{"Beer"}
params["drink_name"] = []string{checkin.Beer.Name}
params["drink_media"] = []string{checkin.Beer.Label.String()}
params["drink_style"] = []string{checkin.Beer.Style}
params["drink_abv"] = []string{fmt.Sprintf("%.4f", checkin.Beer.ABV)}
if checkin.Brewery.Active {
params["drink_active"] = []string{"1"}
}
// brewery
//
// manufacturer_name=Alvarium Beer Company
// manufacturer_active=1
// manufacturer_url=http://www.alvariumbeer.com
// manufacturer_media=https://untappd.akamaized.net/site/brewery_logos/brewery-320859_2721c.jpeg
// manufacturer_category=Brewery
// manufacturer_location=41.6641
// manufacturer_location=-72.7548
// manufacturer_location=New Britain, CT
//
params["manufacturer_name"] = []string{checkin.Brewery.Name}
if checkin.Brewery.Active {
params["manufacturer_active"] = []string{"1"}
}
params["manufacturer_url"] = []string{checkin.Brewery.Contact.URL}
params["manufacturer_media"] = []string{checkin.Brewery.Logo.String()}
params["manufacturer_category"] = []string{"Brewery"}
params["manufacturer_location"] = []string{
fmt.Sprintf("%.4f", checkin.Brewery.Location.Latitude),
fmt.Sprintf("%.4f", checkin.Brewery.Location.Longitude),
strings.Join([]string{
checkin.Brewery.Location.City, checkin.Brewery.Location.State,
}, ", "),
}
// venue
//
// venue_name=Untappd at Home
// venue_media=https://untappd.akamaized.net/venuelogos/venue_9917985_b3a5d245_bg_176.png
// venue_url=https://untappd.com/v/untappd-at-home/9917985
// venue_category=Residence
// venue_location=34.2347
// venue_location=-77.9482
// venue_location=Everywhere, United States
//
if checkin.Venue != nil {
params["venue_name"] = []string{checkin.Venue.Name}
params["venue_media"] = []string{checkin.Venue.Icon.LargeIcon.String()}
params["venue_url"] = []string{checkin.Venue.Foursquare.URL}
params["venue_category"] = []string{checkin.Venue.Category}
params["venue_location"] = []string{
fmt.Sprintf("%.4f", checkin.Venue.Location.Latitude),
fmt.Sprintf("%.4f", checkin.Venue.Location.Longitude),
strings.Join([]string{
checkin.Venue.Location.Address,
checkin.Venue.Location.City,
checkin.Venue.Location.State,
checkin.Venue.Location.Country,
}, ", "),
}
}
// everything in untappd is public
params["public"] = []string{"1"}
return apBuildCheckin(params)
}
func apBuildCheckin(params url.Values) (vocab.SocialtapCheckin, error) {
mediaParams, _ := params["checkin_media"]
ratingParams, _ := params["checkin_rating"]
......@@ -79,6 +186,19 @@ func apBuildCheckin(params url.Values) (vocab.SocialtapCheckin, error) {
_, public := params["public"]
checkin := streams.NewSocialtapCheckin()
publishedProp := streams.NewActivityStreamsPublishedProperty()
if publishedParams, ok := params["published"]; ok && len(publishedParams) > 0 {
published, err := time.Parse(time.RFC3339, publishedParams[0])
if err != nil {
return nil, err
}
publishedProp.Set(published)
} else {
publishedProp.Set(time.Now())
}
checkin.SetActivityStreamsPublished(publishedProp)
if public {
toProp := streams.NewActivityStreamsToProperty()
publicIRI, err := url.Parse(pub.PublicActivityPubIRI)
......
......@@ -64,7 +64,7 @@ SELECT payload, create_time FROM fed_notes
ORDER BY payload->'id'
)
SELECT payload FROM deduped
ORDER BY create_time DESC
ORDER BY payload->'published' DESC
LIMIT $2 OFFSET $3`, vocabType, limit, offset)
}
......@@ -105,7 +105,7 @@ SELECT payload, create_time FROM fed_notes
)
SELECT payload
FROM deduped
ORDER BY create_time DESC
ORDER BY payload->'published' DESC
LIMIT $3 OFFSET $4`, userIRI, vocabType, limit, offset)
}
......
......@@ -2,7 +2,7 @@ module git.feneas.org/zauberstuhl/unmappd
go 1.16
replace github.com/mdlayher/untappd => git.feneas.org/zauberstuhl/untappd-library v0.0.0-20210315203847-7c88ec72b190
replace github.com/mdlayher/untappd => git.feneas.org/zauberstuhl/untappd-library v1.0.0
replace github.com/go-fed/activity => git.feneas.org/socialtap/activity v1.0.7
......
......@@ -19,10 +19,14 @@
package main
import (
// "net/url"
"context"
"time"
"fmt"
"github.com/mdlayher/untappd"
"github.com/go-fed/apcore/util"
"github.com/go-fed/apcore/paths"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
)
......@@ -156,6 +160,35 @@ func insertCheckins(user *UnmappdUser, checkins []*untappd.Checkin) (uint, error
if maxID == 0 || uint(checkin.ID) < maxID {
maxID = uint(checkin.ID)
}
// if the entry did not exist in our database. Lets push this
// to our activitypub stream too.
ctx := util.Context{context.Background()}
userID := fmt.Sprintf(
"%s://%s%susers/%s",
application.Scheme,
application.Config.ServerConfig.Host,
application.Config.UnmappdConfig.SubFolder,
user.ActivityID,
)
apCheckin, err := convertUntappdCheckin(checkin)
if err != nil {
util.ErrorLogger.Warningf("cannot convert untappd checkin: %s", err)
} else {
//actorIRI, err := url.Parse(userID)
//if err != nil {
// return maxID, err
//}
//ctx.WithActorIRI(actorIRI)
if err := application.Framework.Send(
ctx,
paths.UUID(user.ActivityID),
apCheckin,
); err != nil {
util.ErrorLogger.Errorf("cannot send activity (%s): %s", userID, err)
return maxID, fmt.Errorf("UNIQUE") // ends the loop
}
}
}
return maxID, nil
}
......@@ -37,7 +37,7 @@ export default {
intervalIds: []
}
},
mounted () {
beforeMount () {
// register toast channel
this.$root.$on('toast', (title, message, duration = 4000, colorClass = 'success') => {
this.$bvToast.toast(message, {
......
......@@ -9,11 +9,27 @@
.alignment {
align-self: center
}
.checkin-img {
max-height: 350px;
overflow: hidden;
}
.checkin-img .b-icon {
font-size: 10rem;
margin-top: 2rem;
margin-bottom: 2rem;
}
</style>
<template>
<Loading v-if="loading.processing > loading.processed" />
<b-jumbotron v-else bg-variant="light" text-variant="dark" border-variant="light" fluid>
<b-jumbotron
v-else
id="infinity-scroll"
bg-variant="light"
text-variant="dark"
border-variant="light"
fluid
>
<div class="row" v-for="(checkin, index) in checkins" v-bind:key="'checkin' + index">
<div class="col mb-4">
<b-card
......@@ -23,13 +39,14 @@
no-body
>
<b-row no-gutters>
<b-col md="6">
<b-col v-if="index%2===0" md="6" class="alignment checkin-img">
<b-card-img
v-if="checkin.image"
:src="checkin.image.url"
:alt="checkin.image.name"
class="rounded-0"
></b-card-img>
<b-icon-cup-fill v-else></b-icon-cup-fill>
</b-col>
<b-col md="6" class="alignment">
<b-card-body>
......@@ -41,10 +58,12 @@
<a :href="checkin.manufacturer.url">
{{ checkin.manufacturer.name }}
</a>
at
<a :href="checkin.venue.url">
{{ checkin.venue.name }}
</a>
<template v-if="checkin.venue">
at
<a :href="checkin.venue.url">
{{ checkin.venue.name }}
</a>
</template>
</b-card-text>
<b-card-text>
<b-form-rating
......@@ -65,8 +84,20 @@
<b-card-text v-if="geoExists(checkin)" class="text-muted">
{{ geo[checkin.venue.location.latitude][checkin.venue.location.longitude] }}
</b-card-text>
<b-card-text class="text-muted">
{{ moment(checkin.published, 'YYYY-MM-DDThh:mm:ssZ').fromNow() }}
</b-card-text>
</b-card-body>
</b-col>
<b-col v-if="index%2!==0" md="6" class="alignment checkin-img">
<b-card-img
v-if="checkin.image"
:src="checkin.image.url"
:alt="checkin.image.name"
class="rounded-0"
></b-card-img>
<b-icon-cup-fill v-else></b-icon-cup-fill>
</b-col>
</b-row>
</b-card>
</div>
......@@ -86,6 +117,8 @@ export default {
},
data () {
return {
busy: false,
page: 1,
geo: {},
checkins: [],
people: [],
......@@ -96,47 +129,74 @@ export default {
}
},
mounted () {
console.debug(this.$route, this.$routes)
this.updateBeers()
var id = setInterval(() => {
this.updateBeers()
}, 10000)
this.$root.$emit('interval', id)
this.updateBeers(true)
this.$root.$emit('interval', setInterval(this.updateBeers, 10000))
// events
document.addEventListener('scroll', this.infinityScroll)
},
methods: {
updateBeers () {
updateBeers (displayLoadingBar = false) {
ApiAp.get('/checkins').then((resp) => {
if (this.maxEntries) {
this.checkins = resp.data.orderedItems.slice(0, this.maxEntries)
} else {
this.checkins = resp.data.orderedItems
}
this.reverseGeolocation(displayLoadingBar)
this.userInfo(displayLoadingBar)
}).catch((resp) => onError(this, resp))
},
addBeers () {
if (this.maxEntries || this.busy) {
// that is only supported in SPV
return
}
this.busy = true
this.page++ // move to the next page
// XXX kill the update interval
// we need to activate it after the user decided to scroll back up
this.$root.$emit('interval', -1)
// run the background job
ApiAp.get(`/checkins?page=${this.page}`).then(resp => {
for (var idx in resp.data.orderedItems) {
this.checkins.push(resp.data.orderedItems[idx])
}
this.reverseGeolocation()
this.userInfo()
}).catch((resp) => onError(this, resp))
}).catch((resp) => onError(this, resp)).finally(() => { this.busy = false })
},
geoExists (checkin) {
if (!checkin.venue) {
return false
}
var lat = checkin.venue.location.latitude
var lng = checkin.venue.location.longitude
return this.geo[lat] && this.geo[lat][lng]
},
userInfo () {
userInfo (displayLoadingBar = false) {
for (var idx in this.checkins) {
var checkin = this.checkins[idx]
if (this.people[checkin.attributedTo] || !checkin.attributedTo) {
continue
}
this.people[checkin.attributedTo] = {}
this.loading.processing++
if (displayLoadingBar) {
this.loading.processing++
}
ApiAp.get(checkin.attributedTo).then((resp) => {
this.people[checkin.attributedTo] = resp.data
this.loading.processed++
if (displayLoadingBar) {
this.loading.processed++
}
}).catch((resp) => onError(this, resp))
}
},
reverseGeolocation () {
reverseGeolocation (displayLoadingBar = false) {
for (var idx in this.checkins) {
if (!this.checkins[idx].venue) {
continue
}
var lat = this.checkins[idx].venue.location.latitude
var lng = this.checkins[idx].venue.location.longitude
if (this.geoExists(this.checkins[idx])) {
......@@ -144,15 +204,31 @@ export default {
}
this.geo[lat] = {}
this.geo[lat][lng] = ''
this.loading.processing++
if (displayLoadingBar) {
this.loading.processing++
}
ApiAp.get(
`https://nominatim.openstreetmap.org/reverse` +
`?lat=${lat}&lon=${lng}&format=json&addressdetails=0&zoom=8`
).then((resp) => {
this.geo[lat][lng] = resp.data.display_name
this.loading.processed++
if (displayLoadingBar) {
this.loading.processed++
}
}).catch((resp) => onError(this, resp))
}
},
infinityScroll () {
var doc = document.documentElement
if (doc.scrollTop >= (doc.scrollHeight - doc.clientHeight) * 0.9 && !this.busy) {
// will disable auto updates
this.addBeers()
}
// enable auto updates again
if (doc.scrollTop * 0.9 <= 0) {
this.$root.$emit('interval', -1)
this.$root.$emit('interval', setInterval(this.updateBeers, 10000))
}
}
}
}
......
......@@ -3,6 +3,7 @@ import App from '@/App.vue'
import router from '@/router'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import { Api } from '@/api'
import moment from 'moment'
// Import Bootstrap an BootstrapVue CSS files (order is important)
import 'bootstrap/dist/css/bootstrap.css'
......@@ -33,6 +34,9 @@ Vue.use(BootstrapVue)
// Optionally install the BootstrapVue icon components plugin
Vue.use(IconsPlugin)
// register momentjs as prototype variable
Vue.prototype.moment = moment
Api.get('/config').then(response => {
new Vue({
router,
......
......@@ -132,6 +132,14 @@ func token_handler(token string, w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", 301)
return
}
go func() {
err = checkin_worker(&user)
if err != nil {
util.ErrorLogger.Errorln("cannot fetch checkins:", err)
}
}()
http.Redirect(w, r, fmt.Sprintf(userParam, info.UserName), 301)
return
}
......
......@@ -5532,6 +5532,11 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
dependencies:
minimist "^1.2.5"
moment@^2.29.1:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment