Skip to content

Commit

Permalink
Dev twitter api, style list of npubs
Browse files Browse the repository at this point in the history
  • Loading branch information
boreq committed Oct 23, 2023
1 parent 03627ce commit 2e56336
Show file tree
Hide file tree
Showing 23 changed files with 390 additions and 48 deletions.
7 changes: 5 additions & 2 deletions cmd/crossposting-service/di/inject_adapters.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@ var adaptersSet = wire.NewSet(
wire.Bind(new(app.RelayEventDownloader), new(*adapters.RelayEventDownloader)),

twitter.NewTwitter,
twitter.NewNoopOnWriteTwitter,
twitter.NewDevelopmentTwitter,
selectTwitterAdapterDependingOnConfig,

adapters.NewTwitterAccountDetailsCache,
wire.Bind(new(app.TwitterAccountDetailsCache), new(*adapters.TwitterAccountDetailsCache)),
)

func newAdaptersFactoryFn(deps buildTransactionSqliteAdaptersDependencies) sqlite.AdaptersFactoryFn {
Expand Down Expand Up @@ -104,7 +107,7 @@ func newSqliteDB(conf config.Config, logger logging.Logger) (*sql.DB, func(), er
func selectTwitterAdapterDependingOnConfig(
conf config.Config,
productionAdapter *twitter.Twitter,
developmentAdapter *twitter.NoopOnWriteTwitter,
developmentAdapter *twitter.DevelopmentTwitter,
) app.Twitter {
if conf.Environment() == config.EnvironmentDevelopment {
return developmentAdapter
Expand Down
7 changes: 4 additions & 3 deletions cmd/crossposting-service/di/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<template>
<button @click="$emit('click', $event)">{{ text }}</button>
</template>

<script lang="ts">
import {Options, Vue} from 'vue-class-component';
@Options({
props: {
text: String
},
})
export default class Button extends Vue {
text!: string
}
</script>

<style scoped lang="scss">
button {
background-image: linear-gradient(180deg, #F08508 0%, #F43F75 100%);
padding: 19px; // input has 16px padding + 3px border
border: 0;
border-radius: 10px;
color: #FFF;
font-size: 32px;
font-weight: 700;
&:hover {
cursor: pointer;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<img class="checkmark" src="../assets/checkmark.svg">
</template>

<script lang="ts">
import {Options, Vue} from 'vue-class-component';
@Options({})
export default class Checkmark extends Vue {
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@
<img src="../assets/logout_on_dark.svg">
</a>
</div>
<img class="checkmark" src="../assets/checkmark.svg">
<Checkmark></Checkmark>
</div>
</template>

<script lang="ts">
import {Options, Vue} from 'vue-class-component';
import {User} from "@/dto/User";
import Checkmark from "@/components/Checkmark.vue";
@Options({
components: {Checkmark},
props: {
user: User
},
Expand Down Expand Up @@ -48,7 +50,7 @@ export default class CurrentUser extends Vue {
align-items: center;
border: 3px solid #9379BF;
gap: 20px;
gap: 5px 20px;
padding: 20px;
.image {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,43 @@
<template>
<input type="text" :placeholder="placeholder">
<input type="text" :placeholder="placeholder" :value="modelValue" @input="onInput">
</template>

<script lang="ts">
import {Options, Vue} from 'vue-class-component';
@Options({
props: {
placeholder: String
placeholder: String,
modelValue: String,
},
emits: [
'update:modelValue',
],
})
export default class Input extends Vue {
placeholder!: string
modelValue!: string;
onInput(event: InputEvent): void {
this.$emit('update:modelValue', (event.target as HTMLInputElement).value);
}
}
</script>

<style scoped lang="scss">
input {
border-radius: 10px;
border: 3px solid rgba(255, 255,255, 0.15);
padding: .5em;
margin: 1em 0;
border: 3px solid rgba(255, 255, 255, 0.15);
padding: 16px;
background-color: #342255;
min-width: 500px;
color: rgba(255, 255, 255, 0.25);
font-weight: 700;
font-size: 32px;
line-height: 32px;
color: #fff;
&::placeholder {
color: rgba(255, 255, 255, 0.25);
}
}
</style>
85 changes: 61 additions & 24 deletions frontend/nos-crossposting-service-frontend/src/views/HomeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
<div class="home">
<Explanation/>

<div v-if="!loading && !user">
<div v-if="!loadingUser && !user">
<div class="step">
1. Link your X account:
</div>

<LogInWithTwitterButton/>
</div>

<div v-if="!loading && user">
<div v-if="!loadingUser && user">
<div class="step">
1. Logged in as
</div>
Expand All @@ -22,32 +22,24 @@
2. Your nostr identities:
</div>

<Input placeholder="Paste your npub address"></Input>

<div v-if="loading">
Loading...
</div>

<div v-if="!loading && user">
<div class="public-keys-wrapper" v-if="!loadingUser && user">
<div v-if="!publicKeys">
Loading public keys...
</div>

<div v-if="publicKeys">
<ul v-if="publicKeys.publicKeys?.length > 0">
<li v-for="publicKey in publicKeys.publicKeys" :key="publicKey.npub">
{{ publicKey.npub }}
</li>
</ul>

<p v-if="publicKeys.publicKeys?.length == 0">
You haven't added any public keys yet.
</p>
</div>

<input placeholder="npub..." v-model="npub">
<button @click="addPublicKey">Link public key</button>
<ul class="public-keys"
v-if="publicKeys && publicKeys.publicKeys?.length > 0">
<li v-for="publicKey in publicKeys.publicKeys" :key="publicKey.npub">
<div class="npub">{{ publicKey.npub }}</div>
<Checkmark></Checkmark>
</li>
</ul>
</div>

<form>
<Input placeholder="Paste your npub address" v-model="npub"></Input>
<Button text="Add" @click="addPublicKey"></Button>
</form>
</div>
</template>

Expand All @@ -62,10 +54,14 @@ import {APIService} from "@/services/APIService";
import {PublicKeys} from "@/dto/PublicKeys";
import {AddPublicKeyRequest} from "@/dto/AddPublicKeyRequest";
import Input from "@/components/Input.vue";
import Button from "@/components/Button.vue";
import Checkmark from "@/components/Checkmark.vue";
@Options({
components: {
Checkmark,
Button,
CurrentUser,
LogInWithTwitterButton,
Explanation,
Expand All @@ -80,7 +76,7 @@ export default class HomeView extends Vue {
publicKeys: PublicKeys | null = null;
npub = "";
get loading(): boolean {
get loadingUser(): boolean {
return this.store.state.user === undefined;
}
Expand Down Expand Up @@ -112,4 +108,45 @@ export default class HomeView extends Vue {
font-size: 28px;
margin-top: 2em;
}
form, .public-keys-wrapper {
margin: 28px 0;
}
button {
margin-left: 1em;
}
.public-keys {
list-style-type: none;
font-size: 28px;
margin: 0;
padding: 0;
li {
margin: 1em 0;
padding: 0;
color: #9379BF;
font-style: normal;
font-weight: 700;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
.npub, .checkmark {
display: inline-block;
}
.npub {
width: 300px;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,17 @@ import (
"github.com/planetary-social/nos-crossposting-service/service/domain/accounts"
)

type NoopOnWriteTwitter struct {
twitter *Twitter
logger logging.Logger
type DevelopmentTwitter struct {
logger logging.Logger
}

func NewNoopOnWriteTwitter(twitter *Twitter, logger logging.Logger) *NoopOnWriteTwitter {
return &NoopOnWriteTwitter{
twitter: twitter,
logger: logger.New("noopOnWriteTwitter"),
func NewDevelopmentTwitter(logger logging.Logger) *DevelopmentTwitter {
return &DevelopmentTwitter{
logger: logger.New("noopOnWriteTwitter"),
}
}

func (t *NoopOnWriteTwitter) PostTweet(
func (t *DevelopmentTwitter) PostTweet(
ctx context.Context,
userAccessToken accounts.TwitterUserAccessToken,
userAccessSecret accounts.TwitterUserAccessSecret,
Expand All @@ -33,10 +31,15 @@ func (t *NoopOnWriteTwitter) PostTweet(
return nil
}

func (t *NoopOnWriteTwitter) GetAccountDetails(
func (t *DevelopmentTwitter) GetAccountDetails(
ctx context.Context,
userAccessToken accounts.TwitterUserAccessToken,
userAccessSecret accounts.TwitterUserAccessSecret,
) (app.TwitterAccountDetails, error) {
return t.twitter.GetAccountDetails(ctx, userAccessToken, userAccessSecret)
// it is too easy to hit API limits using a free API key during development
return app.NewTwitterAccountDetails(
"Fake Display Name",
"fakeusername",
"https://pbs.twimg.com/profile_images/1544326468490170368/VCPwpDkL_normal.jpg",
)
}
54 changes: 54 additions & 0 deletions service/adapters/twitter_account_details_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package adapters

import (
"sync"
"time"

"github.com/boreq/errors"
"github.com/planetary-social/nos-crossposting-service/service/app"
"github.com/planetary-social/nos-crossposting-service/service/domain/accounts"
)

const cacheTwitterAccountDetailsFor = 15 * time.Minute

type TwitterAccountDetailsCache struct {
entries map[string]twitterAccountDetailsCacheEntry // todo cleanup periodically
entriesLock sync.Mutex
}

func NewTwitterAccountDetailsCache() *TwitterAccountDetailsCache {
return &TwitterAccountDetailsCache{
entries: make(map[string]twitterAccountDetailsCacheEntry),
}
}

func (t *TwitterAccountDetailsCache) Get(accountID accounts.AccountID, updateFn func() (app.TwitterAccountDetails, error)) (app.TwitterAccountDetails, error) {
t.entriesLock.Lock()
defer t.entriesLock.Unlock()

entry, ok := t.entries[accountID.String()]
if ok && entry.isUpToDate() {
return entry.value, nil
}

newValue, err := updateFn()
if err != nil {
return app.TwitterAccountDetails{}, errors.Wrap(err, "error getting new twitter account details")
}

t.entries[accountID.String()] = newTwitterAccountDetailsCacheEntry(newValue)
return newValue, nil
}

type twitterAccountDetailsCacheEntry struct {
t time.Time
value app.TwitterAccountDetails
}

func newTwitterAccountDetailsCacheEntry(value app.TwitterAccountDetails) twitterAccountDetailsCacheEntry {
return twitterAccountDetailsCacheEntry{t: time.Now(), value: value}
}

func (e *twitterAccountDetailsCacheEntry) isUpToDate() bool {
return time.Since(e.t) < cacheTwitterAccountDetailsFor
}
Loading

0 comments on commit 2e56336

Please sign in to comment.