Skip to content

Commit

Permalink
Add basic bot friend management
Browse files Browse the repository at this point in the history
  • Loading branch information
rtm516 committed Jul 2, 2024
1 parent 808a80b commit f1360b9
Show file tree
Hide file tree
Showing 13 changed files with 319 additions and 84 deletions.
1 change: 0 additions & 1 deletion bootstrap/geyser/src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ update-interval: 30
friend-sync:
# The amount of time in seconds to check for follower changes
# This can be no lower than 20 due to xbox rate limits
# unless you turn off auto-unfollow which then you can use 10
update-interval: 20

# Should we automatically follow people that follow us
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import com.rtm516.mcxboxbroadcast.manager.BackendManager;
import com.rtm516.mcxboxbroadcast.manager.BotManager;
import com.rtm516.mcxboxbroadcast.manager.ServerManager;
import com.rtm516.mcxboxbroadcast.manager.database.repository.BotCollection;
import com.rtm516.mcxboxbroadcast.manager.models.BotContainer;
import com.rtm516.mcxboxbroadcast.manager.models.response.BotInfoResponse;
import com.rtm516.mcxboxbroadcast.manager.models.request.BotUpdateRequest;
import com.rtm516.mcxboxbroadcast.manager.models.response.CustomResponse;
import com.rtm516.mcxboxbroadcast.manager.models.response.ErrorResponse;
import com.rtm516.mcxboxbroadcast.manager.models.response.FriendResponse;
import com.rtm516.mcxboxbroadcast.manager.models.response.SuccessResponse;
import jakarta.servlet.http.HttpServletResponse;
import org.bson.types.ObjectId;
Expand All @@ -22,9 +22,6 @@
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

@RestController()
Expand All @@ -34,14 +31,12 @@ public class BotsController {
private final BotManager botManager;
private final ServerManager serverManager;
private final BackendManager backendManager;
private final BotCollection botCollection;

@Autowired
public BotsController(BotManager botManager, ServerManager serverManager, BackendManager backendManager, BotCollection botCollection) {
public BotsController(BotManager botManager, ServerManager serverManager, BackendManager backendManager) {
this.botManager = botManager;
this.serverManager = serverManager;
this.backendManager = backendManager;
this.botCollection = botCollection;
}

@GetMapping("")
Expand Down Expand Up @@ -165,4 +160,41 @@ public String session(HttpServletResponse response, @PathVariable ObjectId botId
response.setStatus(200);
return data;
}

@GetMapping("/{botId:[a-z0-9]+}/friends")
public List<FriendResponse> friends(HttpServletResponse response, @PathVariable ObjectId botId) {
if (!botManager.bots().containsKey(botId)) {
response.setStatus(404);
return null;
}

BotContainer botContainer = botManager.bots().get(botId);

if (!botContainer.isRunning()) {
response.setStatus(400);
return null;
}

response.setStatus(200);
return botContainer.friendManager().lastFriendCache().stream().map(FriendResponse::new).toList();
}

@DeleteMapping("/{botId:[a-z0-9]+}/friends/{xuid:[0-9]{16}}")
public void deleteFriend(HttpServletResponse response, @PathVariable ObjectId botId, @PathVariable String xuid) {
if (!botManager.bots().containsKey(botId)) {
response.setStatus(404);
return;
}

BotContainer botContainer = botManager.bots().get(botId);

if (!botContainer.isRunning()) {
response.setStatus(400);
return;
}

botContainer.friendManager().forceUnfollow(xuid);

response.setStatus(200);
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
package com.rtm516.mcxboxbroadcast.manager.models;

import com.rtm516.mcxboxbroadcast.core.FriendManager;
import com.rtm516.mcxboxbroadcast.core.SessionManager;
import com.rtm516.mcxboxbroadcast.core.configs.FriendSyncConfig;
import com.rtm516.mcxboxbroadcast.core.exceptions.SessionCreationException;
import com.rtm516.mcxboxbroadcast.core.exceptions.SessionUpdateException;
import com.rtm516.mcxboxbroadcast.core.storage.FileStorageManager;
import com.rtm516.mcxboxbroadcast.core.storage.StorageManager;
import com.rtm516.mcxboxbroadcast.manager.BotManager;
import com.rtm516.mcxboxbroadcast.manager.database.model.Bot;
import com.rtm516.mcxboxbroadcast.manager.models.response.BotInfoResponse;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
Expand All @@ -40,6 +36,10 @@ public BotContainer(BotManager botManager, Bot bot) {
status = Status.OFFLINE;
}

public boolean isRunning() {
return status == Status.ONLINE;
}

/**
* Get the bot info
* @return the bot info
Expand Down Expand Up @@ -182,6 +182,15 @@ public StorageManager storageManager() {
return storageManager;
}

/**
* Get the friend manager
*
* @return the friend manager
*/
public FriendManager friendManager() {
return sessionManager.friendManager();
}

/**
* Logger proxy for the bot
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.rtm516.mcxboxbroadcast.manager.models.response;

import com.rtm516.mcxboxbroadcast.core.models.session.FollowerResponse;

public record FriendResponse(
String xuid,
boolean isFollowingCaller,
boolean isFollowedByCaller,
String gamertag,
String presenceState

) {
public FriendResponse(FollowerResponse.Person person) {
this(person.xuid, person.isFollowingCaller, person.isFollowedByCaller, person.gamertag, person.presenceState);
}
}
2 changes: 1 addition & 1 deletion bootstrap/manager/src/ui/src/components/basic/Button.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { forwardRef } from 'react'

const Button = forwardRef(function (props, ref) {
const newProps = { ...props }
const { color, children, className } = props
const { color = 'green', children, className } = props
delete newProps.color
delete newProps.children
delete newProps.className
Expand Down
5 changes: 3 additions & 2 deletions bootstrap/manager/src/ui/src/components/basic/Input.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
function Input (props) {
const newProps = { ...props }
const { label, id } = props
const { label, id, className } = props
delete newProps.label
delete newProps.id
delete newProps.className

return (
<>
<div className='relative'>
<div className={'relative ' + className}>
<input {...newProps} className='peer w-full h-full bg-transparent outline outline-0 transition-colors border-b border-blue-gray-200 focus:border-gray-900 text-sm pt-4 pb-1.5' />
<label htmlFor={id} className='flex w-full h-full select-none pointer-events-none absolute left-0 leading-tight transition-colors -top-2.5 text-sm text-gray-500 peer-focus:text-gray-900'>{label}</label>
</div>
Expand Down
31 changes: 31 additions & 0 deletions bootstrap/manager/src/ui/src/components/specialised/Friend.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { UserCircleIcon, UserMinusIcon } from '@heroicons/react/16/solid'

import Button from '../basic/Button'
import { addNotification } from '../layout/NotificationContainer'
import { useState } from 'react'

function Friend ({ botId, friend, updateData }) {
const [disabled, setDisabled] = useState(false)

const callRemove = () => {
setDisabled(true)
fetch('/api/bots/' + botId + '/friends/' + friend.xuid, { method: 'DELETE' }).then((res) => {
if (!res.ok) {
return addNotification('Failed to remove friend', 'error')
}
updateData()
}).finally(() => setDisabled(false))
}

return (
<>
<div className='flex hover:bg-slate-100 rounded p-2 gap-2 items-center'>
<UserCircleIcon className={'w-4 h-4 ' + (friend.presenceState === 'Online' ? 'text-green-600' : 'text-red-600')} />
<div className='grow content-center'>{friend.gamertag}<small className='text-xs text-gray-400'> {friend.xuid}</small></div>
<Button title='Remove friend' color='red' onClick={() => callRemove()} disabled={disabled}><UserMinusIcon className='size-4' aria-hidden='true' /></Button>
</div>
</>
)
}

export default Friend
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useEffect, useState } from 'react'
import Friend from './Friend'
import Button from '../basic/Button'
import Input from '../basic/Input'
import { ChevronDoubleLeftIcon, ChevronDoubleRightIcon, ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/16/solid'

function FriendPanel ({ botId }) {
const [friends, setFriends] = useState([])
const [page, setPage] = useState(1)
const [maxPage, setMaxPage] = useState(1)
const [filteredFriends, setFilteredFriends] = useState([])
const [query, setQuery] = useState('')

const perPage = 10

const updateData = () => {
fetch('/api/bots/' + botId + '/friends').then((res) => res.json()).then((friendsData) => {
setFriends(friendsData.filter(f => f.isFollowingCaller && f.isFollowedByCaller).sort((f1, f2) => f1.xuid - f2.xuid))
})
}

useEffect(() => {
updateData()
const interval = setInterval(updateData, 2500) // Update every 2.5 seconds
return () => clearInterval(interval)
}, [])

useEffect(() => {
setFilteredFriends(friends.filter(f => f.xuid.includes(query) || f.gamertag.toLowerCase().includes(query.toLowerCase())))
}, [friends, query])

useEffect(() => {
changePage(0) // Make sure the page is still valid when friend data changes
}, [filteredFriends])

const changePage = (pageAdjustment) => {
let newPage = page + pageAdjustment
let maxPage = Math.floor(filteredFriends.length / perPage)

// If there are any friends left over, add another page
if (maxPage * perPage < filteredFriends.length) maxPage++

// Make sure the page is within bounds
if (newPage < 1) newPage = 1
if (maxPage < 1) maxPage = 1
if (newPage > maxPage) newPage = maxPage

setPage(newPage)
setMaxPage(maxPage)
}

return (
<>
<div className='bg-white p-6 rounded shadow-lg max-w-6xl w-full'>
<h3 className='text-3xl text-center'>Friends</h3>
<h4 className='text-xl text-center text-gray-400'>
{friends.length}/1000
</h4>
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder='Search'
className='pb-4'
/>
<div className='flex flex-col'>
{filteredFriends.slice(perPage * (page - 1), perPage * page).map((friend, i) => (
<Friend key={i} botId={botId} friend={friend} updateData={updateData} />
))}
<div className='flex justify-center items-center gap-1'>
<Button onClick={() => changePage(-maxPage)}><ChevronDoubleLeftIcon className='h-5 w-5' aria-hidden='true' /></Button>
<Button onClick={() => changePage(-1)}><ChevronLeftIcon className='h-5 w-5' aria-hidden='true' /></Button>
<div>
{page} / {maxPage}
</div>
<Button onClick={() => changePage(1)}><ChevronRightIcon className='h-5 w-5' aria-hidden='true' /></Button>
<Button onClick={() => changePage(maxPage)}><ChevronDoubleRightIcon className='h-5 w-5' aria-hidden='true' /></Button>
</div>
</div>
</div>
</>
)
}

export default FriendPanel
12 changes: 6 additions & 6 deletions bootstrap/manager/src/ui/src/pages/BotDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Button from '../components/basic/Button'
import ConfirmModal from '../components/modals/ConfirmModal'
import { ExclamationTriangleIcon, QuestionMarkCircleIcon } from '@heroicons/react/24/outline'
import { addNotification } from '../components/layout/NotificationContainer'
import FriendPanel from '../components/specialised/FriendsPanel'

function BotDetails () {
const { botId } = useParams()
Expand Down Expand Up @@ -34,9 +35,7 @@ function BotDetails () {
const [deleteOpen, setDeleteOpen] = useState(false)
const deleteCallback = useRef(() => {})

// const [seenLoginCode, setSeenLoginCode] = useState(false)
const [loginCodeOpen, setLoginCodeOpen] = useState(false)
// const [loginCodeCallback, setLoginCodeCallback] = useState(() => {})
const loginCodeCallback = useRef(() => {})
const seenLoginCode = useRef(false)

Expand All @@ -63,10 +62,10 @@ function BotDetails () {
}
})

fetch('/api/bots/' + botId + '/logs').then((res) => res.text()).then((data) => {
fetch('/api/bots/' + botId + '/logs').then((res) => res.text()).then((logsData) => {
if (!seenLoginCode.current) {
// Check if the second last line contains a link to login
const secondLastLine = data.trim().split('\n').reverse()[1]
const secondLastLine = logsData.trim().split('\n').reverse()[1]
if (secondLastLine.includes('https://www.microsoft.com/link')) {
// Extract the code from the line using regex
const code = secondLastLine.match(/ [A-Z0-9]+ /)[0].trim()
Expand All @@ -82,7 +81,7 @@ function BotDetails () {
}
}

setLogs(data)
setLogs(logsData)
})
})
}
Expand Down Expand Up @@ -199,7 +198,7 @@ function BotDetails () {
<div className='bg-white rounded shadow-lg p-6 w-full flex flex-col gap-4'>
<div className='flex justify-between'>
<div className='font-bold'>Gamertag:</div>
<div>{info.gamertag}</div>
<div><a href={'https://www.xbox.com/play/user/' + encodeURIComponent(info.gamertag)}>{info.gamertag}</a></div>
</div>
<div className='flex justify-between'>
<div className='font-bold'>XUID:</div>
Expand Down Expand Up @@ -246,6 +245,7 @@ function BotDetails () {
<Button color='green' onClick={() => downloadSessionData()}>Download current session data</Button>
</div>
</div>
<FriendPanel botId={botId} />
<div className='bg-white p-6 rounded shadow-lg max-w-6xl w-full'>
<h3 className='text-3xl text-center pb-4'>Settings</h3>
<form className='flex flex-col gap-4' onSubmit={handleSubmit}>
Expand Down
1 change: 0 additions & 1 deletion bootstrap/standalone/src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ session:
friend-sync:
# The amount of time in seconds to check for follower changes
# This can be no lower than 20 due to xbox rate limits
# unless you turn off auto-unfollow which then you can use 10
update-interval: 20

# Should we automatically follow people that follow us
Expand Down
Loading

0 comments on commit f1360b9

Please sign in to comment.